导图社区 网络编程思维导图
《网络编程》通过使用套接字来达到进程间通信目的的编程就是网络编程。windows提供的基于网络编程的就是套接字也就是winsock,同时Winpcap也是一个比较方便的工具。
编辑于2021-08-05 08:00:30网络编程
方向
三个层次
充分理解TCP/IP网络模型和协议
增强异常情况处理能力
写出大规模的并发处理程序
核心
网络协议+操作系统控制IO
多线程处理并发
基础
TCP: 字节流套接字
最大报文长度
UDP:数据报套接字
收发数据函数:recvfrom()、sendto()
接收数据函数回阻塞,客户端建议加上超时处理
最大报文长度
套接字对:(client_ip:port, server_ip:port)
本地套接字
基于本地套接字技术的产品:
docker
kubernetes
概述
单主机垮进程间调用的手段,不走网络协议,比UDP、TCP高效
本地套接字需要传入文件路径,而不用传入地址和端口
应用如果无当前目录权限,则会报错,说明本地套接字需要文本读写权限
在执行bind函数的时候会生成一个文件(本地套接字监听的文件),文件名称:xxx=
编程接口与UDP、TCP一致
常用网络工具:
查询网络状态:netstat和lsof
netstat查询所有套接字信息
lsof查找指定IP地址或端口上打开的套接字进程
tcpdum:抓包工具
提高篇
TIME_WAIT细节
主动关闭连接的一方才会出现TIME_WAIT状态,在接收到被动断开连接方的FIN报文后,主动断开方进入该状态
TIME_WAIT停留时间固定为2MSL(最长分节生命周期),Linux固定为60秒
作用
确保最后的ACK能让被动关闭方收到,让其正常关闭
如果此时主动关闭方已经closed状态,被动关闭方重新发送fin后,主动方只能回复RST操作
连接化身和报文迷走有关系:为让旧连接的重复分节在网络中自然消失
如果没有2MSL,旧连接关闭后,连接化身(套接字对与旧连接一模一样)建立起来,这时候来了一个旧连接过来的报文,报文就会被误认为是新报文的一个TCP分节,此时TCP通讯就会受到影响
如果经历了2MSL,新连接收到的所有TCP分片都是新连接产生的
危害:
内存资源占用,影响不大
端口资源占用,一但端口占用情况过多,会导致新连接无法创建
优化:
net.ipv4.tcp_tw_reuse
在协议安全可控的时候,可复用处于TIME_WAIT下的套接字
只适用与连接发起方(C/S模型中的客户端)
TIME_WAIT状态下的连接创建时间超过1秒
需要打开对TCP时间戳的支持:net.ipv4.tcp_timestamps=1
优雅关掉TCP连接
close(int sockfd)函数
将套接字的引用计数-1(套接字可多进程共享)
如果套接字计数到0,彻底释放套接字,关闭两个方向tcp数据流
输入方向:内核将套接字设置为不可读,读操作作都返回异常
输出方向,尝试将缓冲区所有数据发送出去,然后给对端发送FIN,后续所有写操作都会返回异常
shutdown(int sockfd, int howto)函数
howto有三个值(0,1,2)
0 关闭读方向,所有读操作返回EOF,接收缓冲区数据被丢弃,新数据流返回ack,然后偷偷丢弃数据
1 关闭写方向,直接进入tcp半连接,不管套接字引用计数,直接关闭连接写方向;写缓冲区数据直接发送出去,然后发送FIN,写操作都会报错
2 将0 1都执行一次
close和shutdown区别
close 会关闭连接,并释放连接对应的资源,而shutdown不会释放掉套接字和所有资源
shutdown不受引用计数的影响,直接关连接
close不一定发送FIN报文
Keep-Alive
TCP 的Keep-Alive
每隔一段时间发送一个探测报文,连续几个探测报文没有被响应判定为tcp连接死亡,内核将错误信息反馈给上层应用
默认两小时保活时间、保活时间间隔(75秒)、9次探测
TCP自身的心跳检测机制实时性不强,应用程序可灵活建立这种机制
小数据包处理策略
调用send或write等数据发送后,数据只是被拷贝到了系统内核的缓冲区中,等待协议栈处理,应用程序无法决定何时发出
场景
糊涂窗口综合症
接收端应该等的缓冲区空间大到一定合理值后,再通知发送窗口更新通知
限制大批量的小数据包同时发送
Nagle算法
任意时刻,未被确认的小数据包不能超过一个
小数据包:长度小于最大报文段长度MSS的TCP分组
作用:使连续的小数据包一起发送出去
延时ACK
每个报文收到后,不是立即回复ACK,而是累积需要发送的ACK报文,一起发送出去
写操作合并,批量发送,防止Nagle算法引发副作用
writev(int fileds, const struct iovec *iov, int iovcnt)
readv(int fileds, const struct iovec *iov, int iovcnt)
UDP的connect
作用:
防止服务端未开启时,客户端程序阻塞在revcfrom上
建立UDP链接的上下文,让套接字和目的地址+端口关联起来,使操作系统的内核收到的信息与套接字关联
执行sendto或send函数时,如果目的地址和端口不可达,ICMP报文会返回给才做系统内核,如果提前调用了connect,操作系统会查看哪个UDP套接字与该目的地址和端口有关联,如果有,则在套接字傻姑娘调用recvfrom或recv方法时,可以收到内核返回的“Connection Refused”信息
收发函数
如果对UDPconnect后,发送函数的选择因操作系统而异
建议
用send或write函数来发送,如果使用sendto,则把to地址置0
recv或read函数接收,如果用recvfrom,则把from地址信息置0
性能考虑:
发送流程
不用connect:连接套接字 - 发送报文 - 断开套接字 - 连接套接字 - 发送报文 - 断开套接字
用connect:连接套接字 - 发送报文 - 发送报文
用connect 省去了发送UDP报文时,反复连接套接字和断开套接字的开销
“地址已经被使用”
如果服务端通过ctrl c断开连接后,套接字会在TIME_WAIT状态,无法马上重启,会报“Address alreadly in use”
重用套接字选项
为防止TIME_WAIT下出现套接字化身导致报错,操作系统进行了优化
新连接的SYN序列号比老连接的末序列号大,区分出新老连接
开启tcp_timestamps,使新连接的时间戳比老连接时间戳大
通过以上的方式来复用TIME_WAIT连接
通过SO_RESEADDR套接字选项使用
创建socket和bind之间,调用setsocket(listened, SOL_SOCKET, SO_REUSADDR, 1 , sizeof(1))
最佳实践:
服务端程序在创建socket和bind前,要设置SO_REUSEADDR套接字选项,以便服务端程序能在极短的时间内复用同一个端口启动
区分:tcp_tw_reuse和SO_REUSEADDR
tcp_tw_reuse:内核选项,作用在连接发起方,让TIME_WAIT状态连接1秒后,新连接能复用,防止tcp连接资源不够用
SO_REUSEADDR:用户态选项,告诉内核,如果端口被占用且TCP连接状态处于TIME_WAIT,则可以重用端口
TCP流
TCP是一种流式协议:数据根据send函数的发送语序,将数据分组,复制到缓冲区,网络协议栈根据拥塞窗口、发送窗口、发送缓冲区大小有序发送出去
网络字节排序
数据传输和保存的顺序因系统和协议而异,分为两种
小端字节序:保存数据时,从后往前保存(你好,先保存“好”,再保存“你”),将低字节放到起始位
大端字节保存:保存数据时,从前往后保存(“你好”,先保存“你”,再保存“好”),将高字节放在起始位
通过POSIX下的转换函数抱枕主机和网络字节序一样,进行转换
报文读取和解析
显示编码报文长度:
发送端:报文长度已经知道了,按照预设的格式的长度去发送
接收端:按照约定的格式长度去读去,每次都读取消息的固定长度
readn函数:读取报文预设大小的字节,一但缓冲区为空,函数会阻塞,直到数据到达
特殊字符作为边界:
通过设置特殊字符来作为报文边界,如HTTP,通过回车符或回车+换行符,说明消息读取报文边界了,进行下一部分读取
TCP不总是可靠
“不可靠”的原因
当接受端将发送端的数据保存到读取缓冲区的时候就会回复ack报文,但此时应用程序不一定把缓冲区的数据立即读取出来处理,如果接收端未处理,程序就崩溃了,这部分数据就无法被应用程序继续处理
TCP协议没有提供过多的异常处理细节,反映链路异常能力弱
故障模式
对端无FIN包发送
网络中断
如果程序阻塞在read上,这时候对端网络中断,本端是无法感知(读超时解决)
如果write后,阻塞在read上,在TCP协议栈对外发送数据的时,重传12次后(9分钟),协议栈会发现连接异常,read的调用会返回TIMEOUT
系统崩溃后重启
崩溃后重启,本来阻塞在read操作,重传的TCP分组到达重启系统后,因无TCP连接信息,会发送RST重置分节,对端会提示连接重置
write操作会立即失败
对端有FIN包发送
对端用close或shutdown函数 | 程序崩溃,系统内核代为发送FIN包
对端发送FIN包后,会在缓冲区放入一个“EOF”,但是之前接受缓冲区的有效数据不会受影响,read也不会立刻返回
read能感知到FIN包的发送,让程序正常结束
通过write产生RST(异常),read感知RST(异常)
向已关闭的连接连续写,最终导致RST
收到FIN意味着对端不再发送数据,但是本端还是能继续发,直到对方套接字认为收到的数据包是给一个不存在的连接,会回一个RST包,本端的SIGPIPE如果注册了处理函数,write就会异常退出
检查数据的有效性
对端异常状况
对read函数设置超时处理
对连接添加存活检测
利用多路复用计数自带的超时能力,完成对套接字IO的检查,超过预设时间就进入异常处理
缓冲区处理
防止缓冲区溢出,对长度做好判断
如何理解四次挥手
解释
关闭连接时,每个方向都有一次FIN和ACK
发送场景
进程正常退出
调用close、shutdown(双向)
exit或者main函数返回
进程异常退出
kill -9
发生什么事情
该进程打开的文件描述符都会被系统关闭,导致TCP描述符对应的连接上发送一个FIN
主动关闭方会发送FIN报文给对端,对端收到后,插入一个EOF到读缓冲区(已接收的数据之后),程序需要处理着个异常情况(EOF)
几个问题
MSL是任何IP数据报中在网络存活时间最长的,实现由报文中的TTL决定,Linux上实际设置为30s
listen函数的参数backlog
已完成连接队列的最大长度:表示已建立的连接,正在等待被接收(accept调用返回)
未完成队列的最大长度通过/proc/sys/net/ipv4/tcp_max_syn_backlog修改,默认值128
UDP连接和端口套接字的过程
记录目的地址和端口到套接字的映射关系
删除原来记录的映射关系
UDP不connect,客户端为什么能收到服务端信息
如果不connect,操作系统通过报文,记录了源地址、端口和套接字的映射关系,所以不connect也能收到服务端信息
connect会让操作系统保存目的地址、端口和套接字的映射关系
UDP套接字可以多次connect,作用
重新指定新IP和端口
可以断开一个已连接的套接字,第二次调用connect时,调用方要把套接字地址结构的地址族成员设置为AF_UNSPEC
性能篇
select多路复用
用描述符集合来表示检测的I/O对象,默认最大值为1024(Linux)
select(int maxed, fd_set *readset, fd_set *writeset, fd_set * excepset, const struct timeval)
描述符基数是当前最大描述符+1
select调用完后,要重置待测试集合
特点
不仅可用于套接字、标准输入、输出、异常处理也可以
套接字描述符就绪条件
只要内核通知套接字有数据可读,read函数就不会阻塞
只要内核通知套接字可以写,write函数也不会阻塞
poll多路复用
不限制文件描述符个数
int poll(struct pollfd *fds, unsigned long nods, int timeout)
返回值:
若有文件描述符,返回个数;
若超时,则为0
若出错,则为-1
struct pollfd
int fd
值为-1时,poll函数不会对其进行事件检测,会被忽略
short events
表示多个不同的事件,用二进制掩码位操作完成
short revents
检测结果保留在该字段
非阻塞IO
场景
读操作
若套接字对应接收缓冲区无数据读,非阻塞情况下read调用会立即返回
返回EWOULDBLOCK或EAGIN出错信息,此时要小心处理,比如再次调用read
写操作
接收连接操作
发起连接操作
非阻塞I/O可以使用在很多场景,非阻塞I/O下使用轮询会引起CPU占用率高,一般通过非阻塞I/O和I/O多路复用技术一起使用
套接字可以手动设置为非阻塞
make_nonblocking(int fd)
tcp_nonblocking_server_listen(int port)
epoll
用法
通过监控注册的多个描述字,来进行IO事件的分发处理
API
epoll_create(int size)
创建epoll实例,被用于调用epoll_ctl和epoll_wait
用完后需要使用close函数释放epoll占用的内核资源
size传入大于0的值即可,无特殊含义
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
用于增加或删除监控的事件
参数含义
epfd:传入epoll实例描述字,epoll句柄
op:表示新增或删除一个监控事件,EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD
epoll_event:要注册的事件的文件描述符,比如一个监听套接字
event:表示注册的事件类型
也是用掩码表示,还能传入事件的相关数据
int epoll_wait(int epfd, struct epoll_event * events, int max events, int timeout)
调用者进程被挂起阻塞,等待内核IO事件分发
参数含义
epfd:epoll句柄,epoll描述字
events:返回给用户空间需要处理的IO事件,
timeout:阻塞调用的超时时间,-1不超时,0立即返回即使没有IO事件
事件触发模式
边缘触发:只有一次条件满足的时候会触发,之后就不会传递同类事件了
一般认为边缘触发效率高于条件触发
条件触发:只要满足条件,都触发
C10K问题
操作系统
文件句柄,linux单进程默认支持1024,修改/etc/sysctl.conf文件修改该值
系统内存:占用套接字、发送,接收缓冲区
缓冲区:/proc/sys/net/ipv4/tcp_wmen(tcp_rmen)
默认写16384~4194304 bit
默认读87380~6291456 bit
缓冲区不是主要瓶颈
网络带宽不是主要瓶颈
高并发设计
非阻塞IO + readiness notification + 多线程
reactor模式
基于epoll/poll/select的IO事件分发器,也叫做事件驱动,事件轮询
非阻塞IO + readiness notification + 单线程
epoll,对注册的套接字轮询
异步IO + 多线程
函数调用结束后,不阻塞,立即返回,有操作系统完成后续操作,操作系统完成工作后,产生一个信号或执行一个回调函数
阻塞IO和进程模型
进程
pid_t fork(void) 创建进程
在父进程中返回子进程id,在子进程中返回0
会复制父进程的相关值
子进程退出后,内核还有进程相关信息,需要被父进程回收,若不回收,则会变为僵尸进程,挂到进程号为1的init进程上
子进程退出后回收资源:wait和waitpid函数
返回已终止进程的id号和通过statloc指针返回子进程终止的实际状态:正常终止、被信号杀死、作业停止控制等
wait函数:如果没有已终止的子进程,则会阻塞,直到第一个子进程结束
waitpid函数:无已终止子进程时,可选择是否阻塞
waitpid函数可以指定任意想等待终止的进程ID
处理子进程退出方式一般时注册一个信号处理函数,捕捉信号,然后在信号处理函数里面调用waitpid函数来完成子进程资源回收
程序模型:
服务端:父进程监听套接字,子进程处理连接套接字
注意套接字的关闭梳理
注意子进程回收
阻塞IO+线程模型
线程
每个线程是运行在进程中的一个“逻辑流”,每个进程启动时,都会新建一个主线程,主线程中可以产生子线程
每个线程自己的上下文,包括线程ID,栈、程序计数器、寄存器等
一个进程中的多个线程共享进程的虚拟机地址空间,代码、数据、堆、共享库等
由操作系统内核管理,线程的上下文切换比进程小
切换需要记录代码执行位置,变量等
POSIX线程模型是UNIX处理线程标准接口
pthread_create 创建线程
pthread_self 返回线程tid
pthread_exit 终止线程
pthread_join 待进程终止后回收已终止线程的资源,主线程会阻塞
使用线程池+阻塞队列
poll+单线程
事件驱动模型(reactor模型)
一个无限循环的事件分发线程,reactor线程,使用IO分发技术
所有IO操作注册为事件,每个事件由回调函数处理
网络IO事件工作
read
decode
compute
encode
send
问题点
通过线程解决进程处理效率不高的问题,因为连接交互少的时候,子进程也不能销毁,开销大
通过线程池解决线程上下文切换和来回创建线程的损耗
那么那些交互少的连接占用的线程资源如何解决呢?
single reactor thread
single reactor thread + work threads
耗时的工作扔给work 线程池,reactor 线程专注事件分发和收发数据
防止decode、compute、encode等操作降低reactor效率
主从reactor + work threads模式
单reactor模式下,reactor既分发连接建立,也分发已建立连接的IO,高并发情况下,连接建立成功率可能无法保证
主reactor分发连接建立,分发给从reactor,从reactor进行已建立连接IO的分发,通过主从reactor减少锁开销,因为同套接字事件,主reactor只分发一个从reactor
从reactor数量可根据cpu核数设定,可与cpu数保持一致
Netty采用的正是这种编程模式
epoll+多线程
性能好的原因
每次使用select或poll都需要准备一个刚兴趣的事件集合,传到内核,内核空间中分析并构建相应数据结构完成注册;epoll直接维护了一个全局事件集合,不需要每次重新扫描集合,构建数据结构
poll和select每次都需要重新扫描找到活动的事件;而epoll返回的就是活动列表,在事件多的时候,性能优势尤为明显
异步IO
与同步IO最大的差异
读写拷贝等内核操作是异步的,即read后马上返回,内核自动的读取数据,读取完成后返回一个事件,进行回调即可;相当于新建有个内核线程帮我们去处理拷贝操作
限制linux对aio支持有限,用不到在套接字上
windows上有proactor来做异步IO支持
它的事件分发基于已完成的事件
reactor的事件分发基于待完成的事件