网络编程¶
网络编程步骤¶
TCP¶
-
服务端:socket -> bind -> listen -> accept -> recv/send -> close
- 创建一个socket,用函数socket(),设置SOCK_STREAM
- 设置服务器地址和侦听端口,初始化要绑定的网络地址结构
- 绑定服务器端IP地址、端口等信息到socket上,用函数bind()
- 设置允许的最大连接数,用函数listen()
- 接收客户端上来的连接,用函数accept()
- 收发数据,用函数send()和recv(),或者read()和write()
- 关闭网络连接close(),需要关闭服务端sock和accept产生的客户端sock文件描述符
-
客户端:socket -> connect -> send/recv -> close
- 创建一个socket,用函数socket()
- 设置要连接的对方的IP地址和端口等属性
- 连接服务器,用函数connect()
- 收发数据,用函数send()和recv(),或read()和write()
- 关闭网络连接close()
- 注意
- INADDR_ANY表示本机任意地址,一般服务器端都可以这样写
- accept中接收的是客户端的地址,返回对应当前客户端的一个clisock文件描述符,表示当前客户端的tcp连接
- send和recv中接收的是新建立的客户端的sock地址
UDP¶
- 服务端:socket -> bind -> recvfrom/sendto -> close
- 建立套接字文件描述符,使用函数socket(),设置SOCK_DGRAM
- 设置服务器地址和侦听端口,初始化要绑定的网络地址结构
- 绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定
- 接收客户端的数据,使用recvfrom()函数接收客户端的网络数据
- 向客户端发送数据,使用sendto()函数向服务器主机发送数据
- 关闭套接字,使用close()函数释放资源
- 客户端:socket -> sendto/recvfrom -> close
- 建立套接字文件描述符,socket()
- 设置服务器地址和端口,struct sockaddr
- 向服务器发送数据,sendto()
- 接收服务器的数据,recvfrom()
- 关闭套接字,close()
- 注意
- sendto和recvfrom的第56个参数是sock地址
- 服务器端的recvfrom和sendto都是cli地址
- 客户端sendto是服务器端的地址,最后一个参数是指针,recvfrom是新建的from地址,最后一个参数是整型
- UDP不用listen,accept,因为UDP无连接
- UDP通过sendto函数完成套接字的地址分配工作
- 第一阶段:向UDP套接字注册IP和端口号
- 第二阶段:传输数据
- 第三阶段:删除UDP套接字中注册的目标地址信息
- 每次调用sendto函数都重复上述过程,每次都变更地址,因此可以重复利用同一UDP套接字向不同的目标传输数据
- sendto和recvfrom的第56个参数是sock地址
常用API¶
sendto、recvfrom保存对端的地址
- sendto
- recvfrom
TCP中的accept和connect和listen的关系¶
listen¶
-
listen功能
- listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求
- 参数 backlog 的作用是设置内核中连接队列的长度
- 根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换成LISTEN状态。
-
是否阻塞
- listen()函数不会阻塞,它将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
-
backlog的作用
- backlog是队列的长度,内核为任何一个给定的监听套接口维护两个队列:
- 未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三次握手过程。这些套接口处于 SYN_RCVD 状态。
- 已完成连接队列(completed connection queue),每个已完成 TCP 三次握手过程的客户对应其中一项。这些套接口处于 ESTABLISHED 状态
- 当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,该项就从未完成连接队列移到已完成连接队列的队尾,将建立好的链接自动存储到队列中,如此重复
- backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5
- backlog是队列的长度,内核为任何一个给定的监听套接口维护两个队列:
connect¶
-
connect功能
- 对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。
- connect之后是三次握手
-
是否阻塞
- 通常的情况,客户端的connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。
accept¶
-
accept功能
- accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接(三次握手之后)
-
是否阻塞
- 如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
- 如果,服务器不能及时调用 accept() 取走队列中已完成的连接,队列满掉后会怎样呢?UNP(《unix网络编程》)告诉我们,服务器的连接队列满掉后,服务器不会对再对建立新连接的syn进行应答,所以客户端的 connect 就会返回 ETIMEDOUT
UDP中的connect¶
UDP的connect和TCP的connect完全不同,UDP不会引起三次握手
- 未连接的UDP传输数据
- 第一阶段:向UDP套接字注册IP和端口号
- 第二阶段:传输数据
- 第三阶段:删除UDP套接字中注册的目标地址信息
-
已连接的UDP传输数据
- 第一阶段:向UDP套接字注册IP和端口号
- 第二阶段:传输数据
- 第三阶段:传输数据
-
可以提高传输效率
- 采用connect的UDP发送接受报文可以调用send,write和recv,read操作,也可以调用sendto,recvfrom,此时需要将第五和第六个参数置为NULL或0
-
由已连接的UDP套接口引发的异步错误,返回给他们所在的进程。相反我们说过,未连接UDP套接口不接收任何异步错误给一个UDP套接口,connect后的udp套接口write可以检测发送数据成功与否,直接sendto无法检测
-
多次调用connect拥有一个已连接UDP套接口的进程的作用
- 指定新的IP地址和端口号
- 断开套接口
广播和组播过程¶
- 广播
- 只适用于局域网
- 只能向同一网络中的主机传输数据,
- 组播
- 适用于局域网和广域网(internet)
服务端大量TIMEWAIT或CLOSEWAIT状态¶
首先通过TCP的四次挥手过程分析确定两个状态的出现背景。TIMEWAIT是大量tcp短连接导致的,确保对方收到最后发出的ACK,一般为2MSL;CLOSEWAIT是tcp连接不关闭导致的,出现在close()函数之前。
TIMEWAIT¶
- 可以通过设置SOCKET选项SO_REUSEADDR来重用处于TIMEWAIT的sock地址,对应于内核中的tcp_tw_reuse,这个参数不是“消除” TIME_WAIT的,而是说当资源不够时,可以重用TIME_WAIT的连接
- 修改ipv4.ip_local_port_range,增大可用端口范围,来承受更多TIME
- 设置SOCK选项SO_LINGER选项,这样会直接消除TIMEWAIT
CLOSEWAIT¶
客户端主动关闭,而服务端没有close关闭连接,则服务端产生大量CLOSEWAIT,一般都是业务代码有问题
复位报文段RST¶
- 访问不存在的端口,或服务器端没有启动
- 异常终止连接
- TCP提供了异常终止连接的方法,给对方发送一个复位报文段
- 此时对端read会返回-1,显示错误errno:Connection reset by peer
- 这种错误可以通过shutdown来解决
- 处理半打开连接
- 当某端崩溃退出,此时对端并不知道,若往对端发送数据,会响应一个RST复位报文段
优雅关闭和半关闭¶
概念¶
- 一个文件描述符关联一个文件,这里是网络套接字。
- close会关闭用户应用程序中的socket句柄,释放相关资源,从而触发关闭TCP连接
- 关闭TCP连接,是关闭网络套接字,断开连接
- close只是减少引用计数,只有当引用计数为0的时候,才发送fin,真正关闭连接
- shutdown不同,只要以SHUT_WR/SHUT_RDWR方式调用即发送FIN包
- shutdown后要调用close
- 保持连接的某一端想关闭连接了,但它**需要确保要发送的数据全部发送完毕以后才断开连接**,此种情况下需要使用优雅关闭,一种是shutdown,一种是设置SO_LINGER的close
- 半关闭,是关闭写端,但可以读对方的数据,这种只能通过shutdown实现
close¶
close函数会关闭文件描述符,不会立马关闭网络套接字,除非引用计数为0,则会触发调用关闭TCP连接。
- 检查接收缓冲区是否有数据未读(不包括FIN包),如果有数据未读,协议栈会发送RST包,而不是FIN包。如果套接字设置了SO_LINGER选项,并且lingertime设置为0,这种情况下也会发送RST包来终止连接。其他情况下,会检查套接字的状态,只有在套接字的状态是TCP_ESTABLISHED、TCP_SYN_RECV和TCP_CLOSE_WAIT的状态下,才会发送FIN包
- 若有多个进程调用同一个网络套接字,会将网络套接字的文件描述符+1,close调用只是将当前套接字的文件描述符-1,只会对当前的进程有效,只会关闭当前进程的文件描述符,其他进程同样可以访问该套接字
- close函数的默认行为是,关闭一个socket,close将立即返回,TCP模块尝试把该socket对应的TCP缓冲区中的残留数据发送给对方,并不保证能到达对方
- close行为可以通过SO_LINGER修改
struct linger{
int l_onoff; //开启或关闭该选项
int l_linger; //滞留时间
}
shutdown¶
shutdown没有采用引用计数的机制,会影响所有进程的网络套接字,可以只关闭套接字的读端或写端,也可全部关闭,用于实现半关闭,会直接发送FIN包
- SHUT_RD,关闭sockfd上的读端,不能再对sockfd文件描述符进行读操作,且接收缓冲区中的所有数据都会丢弃
- SHUT_WR,关闭写端,确保发送缓冲区中的数据会在真正关闭连接之前会发送出去,不能对其进行写操作,连接处于半关闭状态
- SHUT_RDWR,同时关闭sockfd的读写
解决TCP粘包¶
什么是TCP粘包¶
由于TCP是流协议,因此TCP接收不能确保每次一个包,有可能接收一个包和下一个包的一部分。
如何解决¶
包头和包体,包头中有包体长度
select可以直接判断网络断开吗¶
不可以。若网络断开,select检测描述符会发生读事件,这时调用read函数发现读到的数据长度为0.
send和recv的阻塞和非阻塞情况¶
send函数返回100,并不是将100个字节的数据发送到网络上或对端,而是发送到了协议栈的写缓冲区,至于什么时候发送,由协议栈决定。
send¶
- 阻塞
- 一直等待,直到写缓冲区有空闲
- 成功写返回发送数据长度
- 失败返回-1
- 一直等待,直到写缓冲区有空闲
- 非阻塞
- 不等待,立即返回,成功返回数据长度
- 返回-1,判断错误码
- 若错误码为EAGAIN或EWOULDBLOCK则表示写缓冲区不空闲
- 若错误码为ERROR,则表示失败
recv¶
- 阻塞
- 一直等待,直到读缓冲区有数据
- 成功写返回数据长度
- 失败返回-1
- 一直等待,直到读缓冲区有数据
- 非阻塞
- 不等待,立即返回,成功返回数据长度
- 返回-1,判断错误码
- 若错误码为EAGAIN或EWOULDBLOCK则表示读缓冲区没数据
- 若错误码为ERROR,则表示失败
- 返回0
- 对端关闭连接
网络字节序和主机序¶
字节序分为大端字节序和小端字节序,大端字节序也称网络字节序,小端字节序也称为主机字节序。
- 大端字节序
- 一个整数的高位字节存储在低位地址,低位字节存储在高位地址
- 小端字节序
- 高位字节存储在高位地址,低位字节存储在低位地址
- 转换API
- htonl 主机序转网络序,长整型,用于转换IP地址
- htons 主机序转网络序,短整型,用于转换端口号
- ntohl 网络序转主机序
- ntohs 网络序转主机序
IP地址分类及转换¶
IP地址分类¶
IP转换¶
字符串表示的点分十进制转换成网络字节序的IP地址
- pton,点分十进制转换成地址
- ntop,地址转换成点分十进制
select实现异步connect¶
通常阻塞的connect 函数会等待三次握手成功或失败后返回,0成功,-1失败。如果对方未响应,要隔6s,重发尝试,可能要等待75s的尝试并最终返回超时,才得知连接失败。即使是一次尝试成功,也会等待几毫秒到几秒的时间,如果此期间有其他事务要处理,则会白白浪费时间,而用非阻塞的connect 则可以做到并行,提高效率。
实现步骤¶
- 创建socket,返回套接字描述符;
- 调用fcntl 把套接字描述符设置成**非阻塞**;
- 调用connect 开始建立连接;
- 判断连接是否成功建立。
判断连接是否成功建立:¶
- 如果为**非阻塞**模式,则调用connect()后函数立即返回,如果连接不能马上建立成功(返回-1),则errno设置为EINPROGRESS,此时TCP三次握手仍在继续。
- 如果connect 返回0,表示连接成功(服务器和客户端在同一台机器上时就有可能发生这种情况)
- 失败可以调用select()检测非阻塞connect是否完成。select指定的超时时间可以比connect的超时时间短,因此可以防止连接线程长时间阻塞在connect处。
- 调用select 来等待连接建立成功完成;
- 如果select 返回0,则表示建立连接超时。我们返回超时错误给用户,同时关闭连接,以防止三路握手操作继续进行下去。
- 如果select 返回大于0的值,并不是成功建立连接,而是表示套接字描述符可读或可写
- 当连接建立成功时,套接字描述符变成可写(连接建立时,写缓冲区空闲,所以可写)
- 当连接建立出错时,套接字描述符变成既可读又可写(由于有未决的错误,从而可读又可写)
- 如果套接口描述符可写,则我们可以通过调用getsockopt来得到套接口上待处理的错误(SO_ERROR)
- 如果连接建立成功,这个错误值将是0
- 如果建立连接时遇到错误,则这个值是连接错误所对应的errno值(比如:ECONNREFUSED,ETIMEDOUT等)。
为什么忽略SIGPIPE信号¶
- 假设server和client 已经建立了连接,server调用了close, 发送FIN 段给client(其实不一定会发送FIN段,后面再说),此时server不能再通过socket发送和接收数据,此时client调用read,如果接收到FIN 段会返回0
- 但client此时还是可以write 给server的,write调用只负责把数据交给TCP发送缓冲区就可以成功返回了,所以不会出错,而server收到数据后应答一个RST段,表示服务器已经不能接收数据,连接重置,client收到RST段后无法立刻通知应用层,只把这个状态保存在TCP协议层。
-
如果client再次调用write发数据给server,由于TCP协议层已经处于RST状态了,因此不会将数据发出,而是发一个SIGPIPE信号给应用层,SIGPIPE信号的缺省处理动作是终止程序。
-
有时候代码中需要连续多次调用write,可能还来不及调用read得知对方已关闭了连接就被SIGPIPE信号终止掉了,这就需要在初始化时调用sigaction处理SIGPIPE信号,对于这个信号的处理我们通常忽略即可
-
往一个读端关闭的管道或者读端关闭的socket连接中写入数据,会引发SIGPIPE信号。当系统受到该信号会结束进程是,但我们不希望因为错误的写操作导致程序退出。
-
通过sigaction函数设置信号,将handler设置为SIG_IGN将其忽略
- 通过send函数的MSG_NOSIGNAL来禁止写操作触发SIGPIPE信号
如何设置文件描述符非阻塞¶
- 通过fcntl设置
int flag = fcntl(fd, F_GETFL); flag |= O_NONBLOCK; fctncl(fd, F_SETFL, flag);