Linux下的网络编程总结.pdf
Linux 网络编程 参考于linux 环境下 C 编程指南 2012-5-5 说明:这篇文章是我通过学习 linux 网络编程时,按照 TCP、UDP 的工作过程进行的总结。当你阅读别人写的源代码时,可能会遇到某些细节不是很了解。我通过网络查找资料,尽可能的把所有的细节都写入其中。其中详细介绍了工作过程中使用到的函数,并对函数的作用作了注释。希望对大家有所帮助。1 1、TCPTCP 套接口编程套接口编程 基于 TCP 的客户机/服务器模式 使用 TCP 协议的客户机/服务器进程的工作过程如下:客户机进程 服务器进程 socket()socket()bind()listen()accept()挂起,直到有客户的连接请求 connect()三段握手过程 send()recv()服务请求 处理服务请求 0 recv()send()应答信号 close()recv()结束连接通知 基于 TCP 协议的客户机/服务器进程图 服务器进程:1、socket():sockfd=socket(AF_INET,SOCK_STREAM,0)函数原型:#include int socket(int family,int type,int protocol);返回值为非负描述字,则表示成功;如果为负值,则表示失败。其中,参数 family指明协议族;参数 type 指明字节流类型;而参数 protocol 一般为 0。功能:生成一个套接口描述字,也称为套接字。参数 family 的取值范围是:AF_LOCAL UNIX 协议族 AF_ROUTE 路由套接口 AF_INET IPv4 协议 AF_INET6 IPv6 协议 AF_KEY 密钥套接口 参数 type 的取值范围:SOCK_STREAM TCP 套接口 SOCK_DGRAM UDP 套接口 SOCK_PACKET 支持数据链路访问 SOCK_RAM 原始套接口 生成套接口描述字(套接字)后,要为套接口的地址数据结构进行赋初值(1)通用套接口地址数据结构 struct sockaddr unit8_t sa_len;sa_family_t sa_family;/*协议族名*/char sa_data14;它包含在头文件中(2)IPv4 套接口地址数据结构 include struct in_addr in_addr_t s_addr;/*32 位 IP 地址,网络字节序*/;struct sockaddr_in uint8 sin_len;sa_family_t sin_family;in_port_t sin_port;/*16 位端口号,网络字节序*/struct in_addr sin_addr;char sin_zero8;/*备用的域,未使用*/;sockaddr_in 结构中成员均以 sin_开头;sin_len 数据长度成员,固定长度为 16 字节,一般不用设置它;sin_family 协议族名 IPv4 为 AF_INET;sin_port TCP 或 UDP 协议的端口号;端口号与 IPv4 地址都是以网络字节序存储的。例:(my_addr 的类型为 struct sockaddr_in)bzero(&my_addr,sizeof(my_addr);/清零 my_addr.sin_family=AF_INET;my_addr.sin_port=htons(MYPORT);/#define MYPORT 3490 my_addr.sin_addr.s_addr=htonl(INADDR_ANY);注释:1 bzero()函数原型:#include void bzero(void*dest,size_t nbytes);功能:将指定的起始地址 dest 的前 nbytes 个字节长设置为 0 2 在网络协议中处理多字节数据时采用的都是网络字节序,而不是主机字节序。要把主机字节序和网络字节序相对应,就要用到提供主机字节序和网络字节序之间相互转换功能的函数。#include uint16_t htonsuint16_t hostvalue;uint32_t htonluint32_t hostvalue;返回的是网络字节序。#include uint16_t ntohsuint16_t netvalue;uint32_t ntohluint32_t netvalue;返回的是主机字节序。以上函数中,h 代表 host,n 代表 network,s 代表 short,l 代表 long。一般情况下,使用 htons 和 ntohs 转换端口号,使用 htonl 和 ntohl 转换 IP 地址。3 在大多数系统中,INADDR_ANY 等于 0。值为 0 的数用大端字节数和小端字节数格式表示都一样,因此不必进行网络字节序和本机字节序的转换,可以直接写成如下形式。serveraddr.sin_addrINADDR_ANY;但是,也许会有少数机器定义 INADDR_ANY 为其他值,这时就必须进行字节序的转换。为了保证程序的通用性和机器的无关性,最好每次都进行转换,以保证程序能在所有机器上使用。2、bind()ret=bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr);函数原型:#include int bind(int sockfd,const struct sockaddr*myaddr,socklen_t addrlen);返回值为 0 表示成功,如果失败则返回-1,并且设置全局变量 errno。参数 sockfd:套接字 参数 my_addr:指向 sockaddr 结构体的指针(该结构体中保存有端口和 IP 地址信息)。参数 addlen:结构体 sockaddr 的长度。功能:当调用 socket 函数创建套接字后,该套接字并没有与本机地址和端口等信息相连,bind 函数将完成这些工作。错误信息:EACCES:地址受到保护,用户非超级用户。EADDRINUSE:指定的地址已经在使用。EBADF:sockfd 参数为非法的文件描述符。EINVAL:socket 已经和地址绑定。ENOTSOCK:参数 sockfd 为文件描述符。3、listen()ret=listen(sockfd,BACKLOG);/#define BACKLOG 10 函数原型:#include#include int listen(int sockfd,int backlog);功能:listen 函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在 TCP 服务器编程中 listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。listen 函数在一般在调用 bind 之后-调用 accept 之前调用。返回:0成功,-1失败 参数 sockfd:被 listen 函数作用的套接字,sockfd 之前由 socket 函数返回。在被 socket 函数返回的套接字 fd 之时,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用 connect 函数,期待它主动与其它进程连接,然后在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用 listen 来完成这件事。参数 backlog:规定内核为此套接口排队的最大选择个数。这个参数涉及到一些网络的细节。在进程正理一个一个连接请求的时候,可能还存在其它的连接请求。因为 TCP 连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个 backlog 告诉内核使用这个数值作为上限。毫无疑问,服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小 30 以内。4、accept()sin_size=sizeof(struct sockaddr_in);con_fd=accept(sockfd,(struct sockaddr*)&their_addr,&sin_size);函数原型:#include#include int accept(int sockfd,struct sockaddr*cliaddr,socklen_t*addrlen);功能:accept 函数用于面向连接类型的套接字类型(SOCK_STREAM 和SOCK_SEQPACKET)。accept 函数将从连接请求队列中获得连接信息,创建新的套接字,并返回该套接字的文件描述符。新创建的套接字用于服务器与客户机的通信,而原来的套接字仍然处于监听状态。它们的区别在于:监听套接口描述字只有一个,而且一直存在,每一个连接都有一个已连接套接口描述字,当连接断开时就关闭该描述字。返回值:成功,返回值非负,返回新的套接字文件描述符 出错将返回-1,sockfd 参数:监听的套接字描述符 cliaddr 参数:指向结构体 sockaddr 的指针 addrlen 参数:cliaddr 参数指向的内存空间的长度。错误信息:EAGAIN:套接字处于非阻塞状态,当前没有连接请求。EBADF:非法的文件描述符。ECONNABORTED:连接中断。EINTR:系统调用被信号中断。EINVAL:套接字没有处于监听状态,或非法的 addrlen 参数。EMFILE:达到进程打开文件描述符限制。ENFILE:达到打开文件数限制。ENOTSOCK:文件描述符为文件的文件描述符。EOPNOTSUPP:套接字类型不是 SOCK_STREAM。注意:bind 函数和 accept 函数的第三个参数是不一样的,这一直困扰着我 5、close()函数原型:#include int close(int sockfd);成功则返回 0,否则返回-1。功能:关闭套接口 其中参数 sockfd 是关闭的套接口描述字。当对一个套接口调用 close()时,关闭该套接口描述字,并停止连接。以后这个套接口不能再使用,也不能再执行任何读写操作,但关闭时已经排队准备发送的数据仍会被发出 使用完一个套接口后,一定要记得将它关掉,任何一个文件读写操作完毕之后,都要关闭它的描述字。客户机进程:如果客户端生成的可执行文件为 tcp_client,可以执行如下命令:./tcp_client hostname 来进行信息的传递。我们可以通过输入的服务器名 hostname 来获取详细信息,这时,我们就会用到一个函数:gethostbyname()函数原型:#include struct hostent*gethostbyname(const char*hostname)功能:实现名字地址到数字地址之间的转换工作,进而获取 IP 地址 返回值:成功:返回一个指向 hostent 结构的指针。失败,返回 NULL,同时设置全局变量 h_error 为相应的值 (一般的 socket 系统调用都将错误代号存放在全局变量 error 中,但是和 host 有关的系统调用,则将错误信息放在 h_error 中)那么,什么是 hostent 结构呢?Struct hostent char*h_name;char*h_aliases;int h_addrtype;int h_length;char*h_addr_list;#define h_addr_list0 结构中各个成员含义如下:h_name:主机的规范名字,该字符数组以“0”结束,因此可以作为字符串直接打印;h_aliases:主机的别名列表,指向一个二维数组,每个数组元素又是一个以“0”结束的字符数组;h_addrtype:返回主机的地址类型,如 IPv4 是 AF_INET,IPv6 是 AF_INET6;h_length:返回地址长度(以字节为单位)如 IPv4 是 4,IPv6 是 16;h_addr_list:主机的一组网络地址列表,使用网络字节顺序;h_addr_list0:主机的第一个网络地址。关于 gethostbyname()具体内容,我还有一个疑问,我已经在 CSDN 上提了出来,如果您是编程高手,希望解答一下:http:/ 1、socket()这个函数前面提过,这里不必多说。创建套接字后,同理,也需要对套接口进行设置:(这是在客户端填充的服务器端的资料)bzero(&server_addr,sizeof(server_addr);/初始化,置 0 server_addr.sin_family=AF_INET;/IPV4 server_addr.sin_port=htons(portnumber);/(将本机器上的 short 数据转化为网络上的 short 数据)端口号,与服务器端 的端口号相同 server_addr.sin_addr=*(struct in_addr*)host-h_addr_list);/IP 地址 注释:1忘记 struct in_addr 的,查看前面 struct sockaddr_in 结构体。2host 就是前面提到的 gethostbyname 函数成功后返回的指向 hostent 结构的指针。2、connect()connect(sockfd,(struct sockaddr*)(&server_addr),sizeof(structsockaddr)函数原型:#include#include int connect(int sockfd,const struct sockaddr*serv_addr,int addrlen);函数功能:创建了一个套接口之后,使客户端和服务器连接。其实就是完成一个有连接协议的连接过程,对于 TCP 来说就是那个三段握手过程。什么是三段握手过程?客户端先用 connect()向服务器发出一个要求连接的信号 SYN1;服务器进程接收到这个信号后,发回应答信号 ack1,同时这也是一个要求回答的信号 SYN2;客户端收到信号 ack1 和 SYN2 后,再次应答 ack2;服务器收到应答信号 ack2,一次连接才算建立完成。从上面过程可以看出,服务器会收到两次信号 SYN1 和 ack2,因此服务器进程需要两个队列保存不同状态的连接。刚接收到 SYN1 信号时,连接还未完成,这时的连接放在一个名为“未完成连接”的队列中。接收到 ack2 信号后,三段握手完成,这时的连接放在名为“已完成连接”的队列中,等待 accept()调用。具体过程如下图:客户端 服务器端 listen()connect()入“未完成连接”队列 connect()返回 入“完成连接”队列 三段握手过程示意图 返回值:成功:返回 0 错误:返回-1,并将全局变量 errno 设置为相应的错误号。参数 sockfd:数据发送的套接字,解决从哪里发送的问题,ockfd 是先前 socket返回的值 参数 serv_addr:据发送的目的地,也就是服务器端的地址 参数 addrlen:指定 server_addr 结构体的长度 2 2、UDPUDP 套接口编程套接口编程 说完 TCP 套接口编程,我们再说说 UDP 套接口编程。不同于 TCP 协议,UDP 提供的是一种无连接的、不可靠的数据包协议。它不对数据进行确认、出错重传、排序等可靠性处理,但是它却具有代码小、速度快和系统开销小等优点。对于某些应用程序,使用 UDP 来实现,将带来更大效率。基于 UDP 的客户机/服务器模式 下图给出的是基于 UDP 的客户机/服务器模式的工作流程图:与基于 TCP 协议的客户机/服务器模式的工作流程图相比较,它们的主要区别 在于:使用 TCP 套接口必须先建立连接(例如客户进程的 connect(),服务器进程的 listen()和 accept()。而 UDP 套接口不需预先连接,它在调用 socket()生成一个套接口后,在服务器端调用 bind()绑定众所周知的端口,服务器阻塞于 recvfrom()调用,客户端调用 sendto()发送数据请求,阻塞于 recvfrom()调用,服务器端调用 recvfrom()接收数据,服务器端也调用 sendto()向客户发送数据作为应答,然后阻塞于 recvfrom()调用,客户端调用 recvfrom()接收数据。当数据传输完成以后,UDP 套接口中的客户端调用close()断开连接,而 TCP 套接口中的客户端不必再发出“断开连接信号”来通知服务器端关闭连接。一些重要的应用程序,如域名服务系统 DNS、网络文件系统 NFS 都使用 UDP 套接口。客户进程 服务器进程 socket()socket()bind()sendto()recvfrom()处理服务请求 应答信号 recvfrom()sendto()close()close()基于 UDP 的客户机/服务器模式的工作流程图 上图中的 socket()、bind()函数就不再继续讲解了,主要讲解 recvfrom()函数和 sendto()函数。1、recvfrom()recvfrom(sockfd,msg,MAX_MSG_SIZE,0,(struct sockaddr*)&addr,&addrlen);函数原型:#include int recvfrom(int sockfd,void*buff,int len,int flags,struct sockaddr*fromaddr,int*addrlen);函数返回实际读的字节数,可以为 0,如果出错,则返回-1。参数 sockfd 为套接口描述字;参数 buff 为指向读缓冲的指针;参数 len 为读的字节数;参数 flags 一般设置为 0;参数 fromaddr 为指向数据接收的套接口地址结构的指针;参数 addrlen 为套接口结构长度。2、sendto()函数原型:#include int sendto(int sockfd,void*mes,int len,int flags,struct sockaddr*toaddr,int*addrlen);函数返回实际写的字节数,可以为 0,如果出错,则返回-1。参数 mes 为指向写缓冲的指针;参数 toaddr 为指向数据发送的套接口地址结构的指针;其他的和上面一样。