linux套接字编程.ppt
第三章 套接字,套接字是一种网络API(应用程序编程接口)。它定义了许多函数和例程,程序员可以用它开发网络应用程序。套接字接口本意在于提供一种进程间通信的方法,使得在相同或不同主机上的进程能以相同的规范进行双向信息传送。进程通过调用套接字接口API来实现相互之间的通信。套接字接口又利用下层的网络通信协议功能和系统调用实现实际的通信工作。,2,进程通信与套接字接口,3,1.套接字是一个主机本地应用程序所创建的,为操作系统所控制的接口(“门”).2.应用进程通过这个接口,使用传输层提供的服务,跨网络发送(/接收)消息到(/从)其他应用进程。3.Client/server模式的通信接口套接字接口.,4,本章内容,3.1 套接字类型3.2 套接字地址结构3.3 位顺序调整3.4 带外数据3.5 连接类型3.6 TCP套接字3.7 UDP套接字3.8 小结,5,3.1 套接字类型,套接字支持各种通信域,即多种不同的通信协议。目前UNIX系统主要支持以下几种协议:UNIX:UNIX系统内部协议INET:IP版本4INET6:IP版本6,6,UNIX系统支持多种套接字类型。套接字类型:是指创建套接字的应用程序所希望的通信服务类型。目前,UNIX系统主要定义如下:SOCK_STREAM:提供可靠的面向连接传输的数据流,保证数据在传输过程中无丢失、无损坏和无冗余。INET地址簇中的TCP协议支持该套接字。SOCK_DGRAM:提供数据的双向传输,但不保证消息地准确到达,即使消息能够到达,也无法保证其顺序性,并可能有冗余或损坏。INET地址簇中的UDP协议支持该套接字。SOCK_RAM:是低于传输层的低级协议或物理网络提供的套接字类型。它可以访问内部网络接口。例如,可以接收和发送ICMP报。SOCK_SEQPACKET:提供可靠的、双向的、顺序化的以及面向连接的数据通信。类似于STREAM方式,但它的报文大小可变(最大报文长度固定)。SOCK_RDM:类似于SOCK_DGRAM,但它可保证数据的正确到达。,7,3.2 套接字地址结构,多数套接字函数需要一个指向地址结构的参数。对应于不同的协议簇,有不同的地址结构。在头文件中定义了以下结构来保持套接字函数调用参数的一致性。struct sockaddr unsigned short sa_family;/*地址类型,格式为AF_XXX*/char sa_data14;/*14字节的协议地址*/;其中的sa_family为套接字的协议簇地址类型,TCP/IP的协议对于IPv4地址类型为AF_INET。sa_data中存储具体的协议地址,不同的协议簇有不同的地址格式。,8,但一般编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构sockaddr_in(在netinet/in.h中定义):struct sockaddr_in unsigned short int sin_len;/*IPv4地址长度*/short int sin_family;/*地址类型*/unsigned short int sin_port;/*存储端口号*/struct in_addr sin_addr;/*存储IP地址*/unsigned char sin_zero8;/*空字节*/;在编程中大多数是使用sockaddr_in这个结构来设置/获取地址信息。,sin_family指代协议族,在TCP套接字编程中只能是AF_INET;,sin_port存储端口号(使用网络字节顺序),数据类型是一个16位的无符号整数类型;,sin_addr存储IP地址,IP地址使用in_addr这个数据结构:struct in_addr unsigned long s_addr;这个数据结构是由于历史原因保留下来,主要用作与以前的格式兼容。这里的s_addr按照网络字节顺序存储IP地址。,sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。,9,在IPv6中套接字地址结构采用另外的定义:#DEFINE SIN6_LEN/*如果系统支持套接字地址结构中的长度成员,则此常值必须定义*/struct sockaddr_in6 unsigned short int sin6_len;/*IPv6结构体长度*/short int sin6_family;/*地址类型*/unsigned short int sin6_port;/*存储端口号,网络字节顺序*/unsigned short int sin6_flowinfo;/*优先级和流量标志,网络字节顺序*/struct in6_addr sin6_addr;/*IPv6地址,网络字节顺序*/;struct in6_addr unsigned long s6_addr;/*128位IPv6地址,网络字节顺序*/;,其中sin6_len是一个无符号的8位整数,表示128位IPv6地址长度;sin6_family是IPv6的地址类型;结构中的成员是有序排列的,都是64位对齐的;sin6_flowinfo的结构是:低24位是流量标号,然后4位的优先级标志,剩下4位保留。,10,比较IPv4和IPv6的地址结构,IPv4地址解构的固定长度是16字节,而IPv6的地址结构的固定长度是24字节。,11,设置地址信息的实例(IPv4)struct sockaddr_in mysock;/*设置sockaddr_in的结构体变量mysock*/mysock.sin_family=AF_INET;/*TCP地址结构*/mysock.sin_port=htons(3490);/*short,NBO*/mysock.sin_addr.s_addr=inet_addr(“192.168.1.221”);/*设置地址为192.168.1.221*/bzero(/*设置sin_zero为8位保留字节*/注意:如果mysock.sin_addr.s_addr=INADDR_ANY,则不指定IP地址(用于server程序)。,12,3.2.1 IP地址转换函数,unsigned long inet_addr(const char*cp);inet_addr将一个点分十进制IP地址字符串转换成32位数字表示的IP地址(网络字节顺序)。char*inet_ntoa(struct in_addr in);inet_ntoa将一个32位数字表示的IP地址转换成点分十进制IP地址字符串。这两个函数互为反函数。,13,3.2.2 套接字地址结构与内核之间的传递,当把套接字地址结构传递给套接字处理函数时,是通过指向结构的指针来传递的。结构的长度也作为参数来传递,其传递的方向可以是从进程到内核,也可以是从内核到进程。,14,从进程到内核传递套接字地址结构由于指针所指结构的大小都传递给内核,所以从进程到内核的数据拷贝量使知道的。进行这样操作的一般有这3个函数:bind,connect和sendto,它们的一个参数是指向套接字地址结构的指针,另一个参数是结构的大小(int)。从内核到进程传递套接字地址结构这里主要用到4个函数:accept,recvfrom,getsockname和getpeername。它们的两个参数分别是指向套接字地址结构的指针和指向表示结构大小的整数的指针。注意:跟上面不同的是第二个参数不再是整型(int),而是一个整型指针(int*),这是由于函数在调用和返回时结构大小不同,不至于使内核在写此结构时产生越界错误而设置的。,15,3.3 位顺序调整,网络中存在多种类型的机器,这些不同类型的机器表示数据的字节顺序是不同的。以16进制数308A,在内存地址0 x1000的存储为例,30,8A,30,8A,0 x1000,0 x1000,0 x1001,0 x1001,小端字节序,大端字节序,网络协议中的数据采用统一的网络字节顺序,因为只有采用统一的字节顺序,才能在不同类型的硬件设备之间正确的发送和接收数据。广域网规定的网络字节顺序采用大端字节顺序方式。,16,系统提供4个函数来进行字节顺序转换:#include“netinet/in.h”unsigned short int htons(unsigned short int hostshort);unsigned long int htonl(unsigned long int hostlong);unsigned short int ntons(unsigned short int netshort);unsigned long int ntonl(unsigned long int netlong);其中。前两个函数将主机字节顺序转换成网络字节顺序;后两个函数将网络字节顺序转换成主机字节顺序。在使用这些函数时,我们不关心主机或网络顺序的真实值到底是大端还是小端,只需要调用适当的函数来对给定值(函数的整型参数)进行主机字节顺序和网络字节顺序的转换,它们的返回值就是经过转换以后的结果。,17,3.3.1 字节处理函数,系统提供两组函数来处理多字节数据,一组函数是以b(byte)开头,和BSD系统兼容的函数;另一组是以mem开头,ANSI C所提供的函数。BSD系统兼容函数:#include void bzero(void*s,int n);void bcopy(const void*src,void*dest,int n);void bcmp(const void*s1,const void*s2,int n);ANSI C函数:#include void*memset(void*s,int c,size_t n);void*memcopy(void*dest,const void*src,size_t n);int memcmp(const void*s1,const void*s2,size_t n);,18,bzero函数将目标中指定数目的字节置为0,这个函数经常用来把套接字地址结构初始化为0,如:bzero(bcopy将指定数目的字节从源src移动到目标dest指定的内存区域;bcmp比较任意两个内存区域,即s1指定的内存区域与s2指定的内存区域的前n个字节,若相同则返回值为0,否则返回值为非0;memset函数将参数s指定的内存区域的前n个字节设置为参数c的内容;memcpy函数等同于bcopy,差别是bcopy可以处理源src和目标dest相重叠的情况,而memcpy则对这种情况没有定义。memcmp与bcmp类似。,19,3.3.2 地址转换函数,地址转换函数负责在ASCII字符串和网络字节顺序的二进制值之间进行地址转换。inet_aton,inet_addr和inet_ntoa函数#include int inet_aton(const char*strptr,struct in_addr*addrptr);in_addr_t inet_addr(const char*strptr);char*inet_ntoa(struct in_addr inaddr);inet_aton函数将strptr所指向的字符串转换成32位的网络字节序二进制值,并存储在指针addrptr指向的in_addr结构体中,若成功,返回1。inet_addr函数,其转换结果作为返回值返回32位二进制网络字节序地址,若转换错,则返回INADDR_NONE。inet_ntoa函数将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串,转换结果存于静态内存中,返回一个指向该地址的指针。,20,inet_pton和inet_ntop函数这两个函数功能基本同上,但它们不仅可处理IPV4的地址,对于128位的IPv6的地址也可以处理。#include int inet_pton(int family,const char*strptr,void*addrptr);const char*inet_ntop(int family,const void*addrptr,char*strptr,size_t len);family即可以是AF_INET,也可以是AF_INET6。inet_pton指针strptr指向需要转换的字符串,addrptr用来存储转换结果。而inet_ntop进行相反的转换,从二进制的数值(addrptr指向)转换成ASCII字符串(strptr指向)。len代表转换目标的大小避免产生溢出缓冲区的错误。,21,3.4 带外数据,带外数据也称为TCP紧急数据。在流套接字的抽象中包括了带外数据这一概念。带外数据是每一对相连流套接字间逻辑上独立的传输通道。带外数据是独立于普通数据传送给用户的。这要求带外数据设备必须支持每一时刻至少一个带外数据消息被可靠的传送。带外数据消息至少包含一个字节。在任何时刻仅有一个带外数据信息等候发送。对于仅支持带内数据的通信协议来说(例如紧急数据是与普通数据在同一序列中发送的),系统通常把紧急数据从普通数据中分离出来单独存放。这就允许用户可以在顺序接收紧急数据和非紧急数据之间作出选择。,22,3.5 连接类型,进程经网络进行通信时,有两种方法可以选择:面向连接的方式:即虚电路方式。这种方式是在两个连接端点之间建立一条虚电路,两端点之间的链路可以看作是直接的点到点的连接。两端点间只有在建立连接后才能传输数据。一旦连接建立,双方均可向对方发送非格式化的、可靠的字符流。例如远程登录就是采用这种方式。无连接方式:即数据报方式。在传输报文前,不用建立连接。无连接协议的每个报文包含一个完整的传送地址。数据按数据包形式传输。因此,某一进程可发送信息给某一网络地址,然后再发信息给另一网络地址。对应于进程的两种通信方式,套接字编程也有两种模式。面向连接的模式面向无连接的编程模式,23,3.6 TCP套接字,TCP套接字实现过程服务器端步骤创建套接字绑定套接字设置套接字为监听模式,进入被动接受连接请求状态接受请求,建立连接读/写数据终止连接客户端步骤创建套接字与远程服务程序连接读/写数据终止连接,24,25,#include#include#include main()int sockfd,connect_sock;/*创建TCP套接字*/if(sockfd=socket(AF_INET,SOCK_STREAM,0)=-1)perror(“creating socket failed.”);exit(1);/*绑定套接字*/*监听*/loop/*接受请求连接*/if(connect_sock=accept(sockfd,NULL,NULL)=-1)perror(“Acception failed.”);exit(1);/*创建子进程或线程以服务客户端*/,TCP服务器模板,26,TCP客户端模板,#include#include#include main()int sockfd;/*创建TCP套接字*/if(sockfd=socket(AF_INET,SOCK_STREAM,0)=-1)perror(“creating socket failed.”);exit(1);/*连接服务器和接收来自服务器的数据*/,27,3.6.2 实现TCP套接字,对于基于TCP的通信,无论是服务器还是客户,都必须首先产生其TCP通信传输端点,即TCP套接字。应用程序通过调用socket()产生套接字。该函数调用必须给出所使用的地址簇、套接字类型和协议标志。该函数返回一个套接字描述符。由于系统中套接字也是一种文件,所以套接字描述符是一种文件描述符。之后的任何I/O操作都是作用于该套接字描述符。其数据结构包括一个网络连接的5种信息:通信协议、本地协议地址、本机主机端口、远程主机地址和远程协议端口。,28,socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。,#include int socket(int family,int type,int protocol);返回:非负描述字成功-1失败,第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);如果套接口类型不是原始套接口,那么第三个参数就为0。,29,connect函数:当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。,#include int connect(int sockfd,const struct sockaddr*servaddr,socklen_t addrlen);返回:0成功-1失败,第一个参数是socket函数返回的套接口描述字;第二和第三个参数分别是一个指向套接口地址结构的指针和该结构的大小。,30,bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。,#include int bind(int sockfd,const struct sockaddr*myaddr,socklen_t addrlen);返回:0成功-1失败,第一个参数是socket函数返回的套接口描述字;第二和第第三个参数分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。,31,listen函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。,#include int listen(int sockfd,int backlog);返回:0成功-1失败,第一个参数是socket函数返回的套接口描述字;第二个参数规定了内核为此套接口排队的最大连接个数。由于listen函数第二个参数的原因,内核要维护两个队列:以完成连接队列和未完成连接队列。未完成队列中存放的是TCP连接的三路握手为完成的连接,accept函数是从以连接队列中取连接返回给进程;当以连接队列为空时,进程将进入睡眠状态。,32,accept函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。,#include int accept(int sockfd,struct sockaddr*cliaddr,socklen_t*addrlen);返回:非负描述字成功-1失败,第一个参数是socket函数返回的套接口描述字;第二个和第三个参数分别是一个指向连接方的套接口地址结构和该地址结构的长度;该函数返回的是一个全新的套接口描述字;如果对客户段的信息不感兴趣,可以将第二和第三个参数置为空。,33,3.6.3 数据传输,int Send(int sockfd,const void*data,int data_len,unsigned int flags),功能:在TCP连接上发送数据,返回成功传送数据的长度,出错时返回1。send会将外发数据复制到OS内核中,也可以使用send发送面向连接的UDP报文。参数说明:sockfd:套接字描述符data:指向要发送数据的指针data_len:数据长度flags:一直为0举例(p50):send(s,req,strlen(req),0);,34,int Sendto(int sockfd,const void*data,int data_len,unsigned int flags,struct sockaddr*remaddr,int remaddr_len),功能:基于UDP发送数据报,返回实际发送的数据长度,出错时返回1参数说明:sockfd:套接字描述符data:指向要发送数据的指针data_len:数据长度flags:一直为0remaddr:远端地址:IP地址和端口号remaddr_len:地址长度举例:sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr*),35,int recv(int sockfd,void*buf,int buf_len,unsigned int flags);,功能:从TCP接收数据,返回实际接收的数据长度,出错时返回1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃。也可以使用recv接收面向连接的UDP的报文参数说明:Sockfd:套接字描述符Buf:指向内存块的指针Buf_len:内存块大小,以字节为单位flags:一般为0 举例:recv(sockfd,buf,8192,0),36,int recvfrom(int sockfd,void*buf,int buf_len,unsigned int flags,struct sockaddr*from,int fromlen);,功能:从UDP接收数据,返回实际接收的字节数,失败时返回1参数说明:Sockfd:套接字描述符buf:指向内存块的指针buf_len:内存块大小,以字节为单位flags:一般为0from:远端的地址,IP地址和端口号fromlen:远端地址长度举例:recvfrom(sockfd,buf,8192,0,(struct sockaddr*),37,3.6.4 服务进程的创建,服务器端的进程需要对客户来的请求随时进行响应,因此需要服务器创建新进程的一些函数。fork()函数#include int fork(void);fork()函数最特殊的地方就是调用一次返回两次:父进程的返回值是所创建的子进程的ID值;子进程的返回值是0。所以可以从返回值来判断这两次返回是在哪个进程完成的。若返回-1,则调用出错。,38,fork()函数一般有两个用途:父进程可以通过调用fork()函数来建立自己的拷贝子进程,从而在一个子进程执行任务时另外的子进程同时执行其他的操作。如果进程需要执行其他程序,则应该首先用fork()生成一个拷贝,然后这个拷贝子进程调用exec()函数来执行应用程序。父进程调用fork()前打开的所有套接字描述符都与子进程共享。一般服务器调用accept()后调用fork(),然后子进程继承了accept()的已连接套接字描述符并与之进行通信,而父进程则可以关闭这个套接字。,39,exec()函数用来执行以文件形式存储在磁盘上的可执行程序。#include int execl(const char*pathname,const char*arg(),.);int execv(const char*pathname,char*const argv,.);int execle(const char*pathname,const char*arg(),.);int execve(const char*pathname,char*const argv,char*const envp);int execlp(const char*filename,const char*arg(),.);int execvp(const char*filename,char*const argv);pathname代表一个完整可执行程序的路径名,函数execlp和execvp相应的参数是filename,是根据 PATH 环境变量转换成完整路径名;arg()和arg是命令行参数,空指针用来表示可变数量参数的终止。函数若成功则无返回,若出错则返回-1。,40,3.6.5 终止连接,close()函数用来关闭一个套接字描述符。当完成了套接字的建立、绑定、连接和通信读写之后,都需要最后关闭建立的套接字。#include int close(int sockfd);Sockfd是需要关闭的套接字描述符。若执行成功则返回0,否则返回-1。清除套接字描述符的具体操作:将这个套接字描述符标记为CLOSED状态,然后立即返回进程。TCP协议将继续使用这个套接字,将尚未发送的数据传递到对方,然后发送FIN数据段,关闭和释放这个套接字。这是套接字才被完全删除。,41,shutdown()函数可以部分关闭socket连接,单方面的中断连接,即禁止某个方向的信息传递。#include int shutdown(int sockfd,int how);sockfd代表需要关闭的套接字描述符;how的值:0:禁止接收信息1:禁止发送信息2:接收和发送都被禁止,与close()函数效果相同。,42,close()和shutdown()的区别:close()函数调用时将访问计数减1,只有当访问计数减少到0以后,才关闭套接字;而shutdown()函数可直接发送FIN数据段以终止网络连接,而不管访问计数的多少。shutdown()实际上只关闭了TCP连接的一半。因为TCP连接是双向的,即包括读和写两个方向。Shutdown()函数可以给服务器发送一个FIN,告诉它已经完成了数据发送,但仍为读而开放套接字描述符。Close()函数则是同时关闭了读写两个方向。,43,3.6.6 连接地址信息获取,gethostname(),44,套接字API中的主要系统调用,read和write在UNIX和Linux中,可以代替recv和send,因为都调用内核的sosend实现。小结:,socket*connect*send(write)*recv(read)*close*bind,listenacceptrecvmsgsendmsgrecvfromsendto,shutdowngetpeernamegetsockoptsetsockopt,