- 网络编程的步骤
- 常用API
- TCP中的accept和connect和listen的关系
- UDP中的connect
- 广播和组播过程
- 服务端大量TIMEWAIT或CLOSEWAIT状态
- 复位报文段RST
- 优雅关闭和半关闭
- 解决TCP粘包
- select可以判断网络断开吗
- send和read的阻塞和非阻塞情况
- 网络字节序和主机序
- IP地址分类及转换
- select实现异步connect
- 为什么忽略SIGPIPE信号
- 如何设置非阻塞
服务端:socket -> bind -> listen -> accept -> recv/send -> close
客户端:socket -> connect -> send/recv -> close
注意
sendto、recvfrom保存对端的地址
listen功能
是否阻塞
backlog的作用
connect功能
是否阻塞
accept功能
是否阻塞
UDP的connect和TCP的connect完全不同,UDP不会引起三次握手
未连接的UDP传输数据
已连接的UDP传输数据
可以提高传输效率
采用connect的UDP发送接受报文可以调用send,write和recv,read操作,也可以调用sendto,recvfrom,此时需要将第五和第六个参数置为NULL或0
由已连接的UDP套接口引发的异步错误,返回给他们所在的进程。相反我们说过,未连接UDP套接口不接收任何异步错误给一个UDP套接口,connect后的udp套接口write可以检测发送数据成功与否,直接sendto无法检测
多次调用connect拥有一个已连接UDP套接口的进程的作用
首先通过TCP的四次挥手过程分析确定两个状态的出现背景。TIMEWAIT是大量tcp短连接导致的,确保对方收到最后发出的ACK,一般为2MSL;CLOSEWAIT是tcp连接不关闭导致的,出现在close()函数之前。
客户端主动关闭,而服务端没有close关闭连接,则服务端产生大量CLOSEWAIT,一般都是业务代码有问题
close函数会关闭文件描述符,不会立马关闭网络套接字,除非引用计数为0,则会触发调用关闭TCP连接。
struct linger{
int l_onoff; //开启或关闭该选项
int l_linger; //滞留时间
}
shutdown没有采用引用计数的机制,会影响所有进程的网络套接字,可以只关闭套接字的读端或写端,也可全部关闭,用于实现半关闭,会直接发送FIN包
由于TCP是流协议,因此TCP接收不能确保每次一个包,有可能接收一个包和下一个包的一部分。TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
1)"hello give me sth abour yourself"
2)"Don't give me sth abour yourself"
那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hello give me sth abour yourselfDon’t give me sth abour yourself" 这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
发送方原因
:发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。也就是说发送方需要等发送缓冲区满才发送出去。接收方原因
:TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。不可以。若网络断开,select检测描述符会发生读事件,这时调用read函数发现读到的数据长度为0.
send函数返回100,并不是将100个字节的数据发送到网络上或对端,而是发送到了协议栈的写缓冲区,至于什么时候发送,由协议栈决定。
字节序分为大端字节序和小端字节序,大端字节序也称网络字节序,小端字节序也称为主机字节序。
字符串表示的点分十进制转换成网络字节序的IP地址
通常阻塞的connect 函数会等待三次握手成功或失败后返回,0成功,-1失败。如果对方未响应,要隔6s,重发尝试,可能要等待75s的尝试并最终返回超时,才得知连接失败。即使是一次尝试成功,也会等待几毫秒到几秒的时间,如果此期间有其他事务要处理,则会白白浪费时间,而用非阻塞的connect 则可以做到并行,提高效率。
假设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信号
int flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fctncl(fd, F_SETFL, flag);
select就是用户区用一个bitmap的监听集合rset来存放各个连接过来的文件描述符,在进入select函数后,内核会将该监听集合拷贝一份放入内核区fdset,然后由内核区来轮询遍历该集合,从而找到有读事件发生的文件描述符,接着将rset中该位置位,然后返回。如果没有事件满足读事件,那么select会一直轮询检查,直到有读事件满足,所以select是阻塞的。返回后,程序需要遍历文件描述符,找到对应的读事件,并做处理。当所有的事件处理完之后,将rset清空重新进行初始化。接着进行select循环。
所以:select有如下缺点:
poll相对于select几乎一样,主要区别在于,poll使用一个结构体来表示文件描述符,而不是一个bitmap位图,结构体有三个成员,分别是fd,events,revents。使用结构体数组来存放事件,这样就解决了select的1024的大小限制,另外,poll结构体里的revents成员是表示有无事件发生,置位也只是改变这一位,那么在处理完事件后只需要改变revents就行,这样就避免了不能重用的问题。因而poll解决了select的前两个问题。另外,poll也是阻塞的。
因为select和poll都是通过遍历整个文件描述符表来查找是哪个或哪几个文件描述符有事件发生,所以当并发连接数量很大,而只有少量活跃时,是很浪费CPU资源的。
当内核初始化epoll的时候(当调用epoll_create的时候内核也是个epoll描述符创建了一个文件,毕竟在Linux
中一切都是文件,而epoll面对的是一个特殊的文件,和普通文件不同),会开辟出一块内核高速cache区,这块区
域用来存储我们要监管的所有的socket描述符,当然在这里面存储一定有一个数据结构,这就是红黑树,由于红黑树
的接近平衡的查找,插入,删除能力,在这里显著的提高了对描述符的管理。
epoll是这么做的,epoll是由红黑树实现的,一个epollfd充当树根,其他的文件描述符都是树上的节点,通过epoll_ctl来添加、删除、改变监听节点,当epoll_wait监听到有事件发生时,他会将就绪链表中有事件发生文件描述符换到前面,并返回有事件发生的文件描述符的个数,这样,只需要遍历前面几个文件描述符就行了,无需遍历整个文件描述符表。
当内核创建了红黑树之后,同时也会建立一个双向链表rdlist,用于存储准备就绪的描述符,当调用epoll_wait的
时候在timeout时间内,只是简单的去管理这个rdlist中是否有数据,如果没有则睡眠至超时,如果有数据则立即返
回并将链表中的数据赋值到events数组中。这样就能够高效的管理就绪的描述符,而不用去轮询所有的描述符。所以
管理的描述符很多但是就绪的描述符数量很少的情况下如果用select来实现的话效率可想而知,很低,但是epoll的
话确实是非常适合这个时候使用。对与rdlist的维护:当执行epoll_ctl时除了把socket描述符放入到红黑树中之
外,还会给内核中断处理程序注册一个回调函数,告诉内核,当这个描述符上有事件到达(或者说中断了)的时候就调
用这个回调函数。这个回调函数的作用就是将描述符放入到rdlist中,所以当一个socket上的数据到达的时候内核就
会把网卡上的数据复制到内核,然后把socket描述符插入就绪链表rdlist中。
注意,很多博客说epoll_wait返回时,对于就绪的事件,epoll使用的是共享内存的方式,即用户态和内核态都指向了就绪链表,所以就避免了内存拷贝消耗。epoll_wait的实现~有关从内核态拷贝到用户态代码.可以看到__put_user这个函数就是内核拷贝到用户空间.分析完整个linux ②.⑥版本的epoll实现没有发现使用了mmap系统调用,根本不存在共享内存在epoll的实现。