微信用大型网站站做跳板,新手适合在哪个平台开网店,电商从零基础怎么学,北京网站建设推荐安徽秒搜科技网络编程 网络编程的步骤常用APITCP中的accept和connect和listen的关系UDP中的connect广播和组播过程服务端大量TIMEWAIT或CLOSEWAIT状态复位报文段RST优雅关闭和半关闭解决TCP粘包select可以判断网络断开吗send和read的阻塞和非阻塞情况网络字节序和主机序IP地址分类及转换sel…网络编程 网络编程的步骤常用APITCP中的accept和connect和listen的关系UDP中的connect广播和组播过程服务端大量TIMEWAIT或CLOSEWAIT状态复位报文段RST优雅关闭和半关闭解决TCP粘包select可以判断网络断开吗send和read的阻塞和非阻塞情况网络字节序和主机序IP地址分类及转换select实现异步connect为什么忽略SIGPIPE信号如何设置非阻塞 网络编程步骤
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不用listenaccept因为UDP无连接UDP通过sendto函数完成套接字的地址分配工作 第一阶段向UDP套接字注册IP和端口号第二阶段传输数据第三阶段删除UDP套接字中注册的目标地址信息 每次调用sendto函数都重复上述过程每次都变更地址因此可以重复利用同一UDP套接字向不同的目标传输数据
常用API
sendto、recvfrom保存对端的地址
sendtorecvfrom
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
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一般为2MSLCLOSEWAIT是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包若有多个进程调用同一个网络套接字会将网络套接字的文件描述符1close调用只是将当前套接字的文件描述符-1只会对当前的进程有效只会关闭当前进程的文件描述符其他进程同样可以访问该套接字close函数的默认行为是关闭一个socketclose将立即返回TCP模块尝试把该socket对应的TCP缓冲区中的残留数据发送给对方并不保证能到达对方close行为可以通过SO_LINGER修改
struct linger{int l_onoff; //开启或关闭该选项int l_linger; //滞留时间
}l_onoff为0该选项不起作用采用默认close行为l_onoff不为0 l_linger为0close立即返回TCP模块丢弃被关闭的socket对应的TCP缓冲区中的数据给对方发送RST复位信号这样可以异常终止连接且完全消除了TIME_WAIT状态l_linger不为0 阻塞socket被关闭的socket对应TCP缓冲区若还有数据close会阻塞进程睡眠直到收到对方的确认或等待l_linger时间若超时仍未收到确认则close返回-1设置errno为EWOULDBLOCK非阻塞socketclose立即返回需要根据返回值和errno判断残留数据是够发送完毕
shutdown
shutdown没有采用引用计数的机制会影响所有进程的网络套接字可以只关闭套接字的读端或写端也可全部关闭用于实现半关闭会直接发送FIN包
SHUT_RD关闭sockfd上的读端不能再对sockfd文件描述符进行读操作且接收缓冲区中的所有数据都会丢弃SHUT_WR关闭写端确保发送缓冲区中的数据会在真正关闭连接之前会发送出去不能对其进行写操作连接处于半关闭状态SHUT_RDWR同时关闭sockfd的读写
解决TCP粘包
什么是TCP粘包 由于TCP是流协议因此TCP接收不能确保每次一个包有可能接收一个包和下一个包的一部分。TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包从接收缓冲区来看后一包数据的头紧接着前一包数据的尾出现粘包的原因是多方面的可能是来自发送方也可能是来自接收方。 如果双方建立连接需要在连接后一段时间内发送不同结构数据如连接后有好几种结构 1)hello give me sth abour yourself
2)Dont give me sth abour yourself那这样的话如果发送方连续发送这个两个包出去接收方一次接收可能会是hello give me sth abour yourselfDon’t give me sth abour yourself 这样接收方就傻了到底是要干嘛不知道因为协议没有规定这么诡异的字符串所以要处理把它分包怎么分也需要双方组织一个比较好的包结构所以一般可能会在头加一个数据长度之类的包以确保接收。
粘包出现的原因
发送方原因发送端为了将多个发往接收端的包更加高效的的发给接收端于是采用了优化算法Nagle算法将多次间隔较小、数据量较小的数据合并成一个数据量大的数据块然后进行封包。也就是说发送方需要等发送缓冲区满才发送出去。接收方原因TCP将接收到的数据包保存在接收缓存里然后应用程序主动从缓存读取收到的分组。这样一来如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度多个包就会被缓存应用程序就有可能读取到多个首尾相接粘到一起的包。
如何解决
发送方于发送方造成的粘包问题可以通过关闭Nagle算法来解决使用TCP_NODELAY选项来关闭算法。接收方接收方没有办法来处理粘包现象只能将问题交给应用层来处理。应用层循环处理应用程序从接收缓存中读取分组时读完一条数据就应该循环读取下一条数据直到所有数据都被处理完成但是如何判断每条数据的长度呢 格式化数据每条数据有固定的格式开始符结束符这种方法简单易行但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。发送长度发送每条数据时将数据的长度一并发送例如规定数据的前4位是数据的长度应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
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);select/poll/epoll原理
select
select就是用户区用一个bitmap的监听集合rset来存放各个连接过来的文件描述符在进入select函数后内核会将该监听集合拷贝一份放入内核区fdset然后由内核区来轮询遍历该集合从而找到有读事件发生的文件描述符接着将rset中该位置位然后返回。如果没有事件满足读事件那么select会一直轮询检查直到有读事件满足所以select是阻塞的。返回后程序需要遍历文件描述符找到对应的读事件并做处理。当所有的事件处理完之后将rset清空重新进行初始化。接着进行select循环。
所以select有如下缺点
bitmap最大为1024位内核空间的fdset不能重用需要从用户态拷贝rset到内核态fdset返回后还需要遍历rset才能找到对应的读事件
poll
poll相对于select几乎一样主要区别在于poll使用一个结构体来表示文件描述符而不是一个bitmap位图结构体有三个成员分别是fdeventsrevents。使用结构体数组来存放事件这样就解决了select的1024的大小限制另外poll结构体里的revents成员是表示有无事件发生置位也只是改变这一位那么在处理完事件后只需要改变revents就行这样就避免了不能重用的问题。因而poll解决了select的前两个问题。另外poll也是阻塞的。
epoll
因为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的实现。