计算机网络基础
TCP连接
Unix中的EOF
,可以参考这里:https://en.wikipedia.org/wiki/End-of-file
总结为:读取数据时,数据流结束的标记。比如在读取标准输入的时候,我们输入EOF符号,来通知程序终端输入结束了,因为程序不知道标准输入什么时候为结束。同样的,TCP是面向流的,因此也需要EOF机制来通知通信的对方,自己的数据流结束了。
3次握手的过程:
SYN
包体中,不携带任何数据信息,只是在包头中包含一些必要的信息,一般是以下3个:
- MSS:报文的最大长度
- 窗口规模:滑动窗口中,窗口的大小
- 时间戳:防止有些网络抖动产生的延迟分组等的干扰
ACK
是对上一次SYN
的一个应答,在上次SYN
的数字上+1
数据传输的时候,TCP根据序列号来保证数据的有序的,即A发送给B一个数据段,序列号是x,则B需要回复x+1来通知A这个数据包已经到达。如果A没有收到x+1的回复,就需要重复发送数据包。
第一次握手:s确认自己的和c的连接是正常的,而c什么都不知道。c进入SYN_SENT
状态,s进入SYN_RCVD
状态
第二次握手:c确认自己的连接是正常的,而s不知道c是否成功了。
第三次握手:s知道c是正常的,此时双方进入ESTABLISH
状态。
如果2次握手,那么如果s回复的ack丢失,s这里会开启大量的连接,造成资源浪费。
为什么不是4次、5次等。因为计算机网络的基本原理告诉我们,完全可靠的连接是不存在的,因此3次一般是最佳的选择。
4次挥手的过程:
TCP是面向流的,因此通信的双发需要知道对方的数据已经结束了,Unix中一般是EOF作为标记。
第一次挥手:c发送FIN,里面包含了EOF字段,同时s自己没有数据可以发送了
第二次挥手:s发送ACK,回应上次的c的SYN
第三次挥手:s发送FIN,里面包含EOF,通知c自己没有数据可以发送了
第四次挥手:c发送ACK,回应上次的FIN
整体流程
RST和RST攻击
RST表示复位标记,发送RST包时,意味着以下的几个状态:
- 发送方发送rst时,不必等缓冲区的数据都发送完,就立刻关闭连接
- 接收方收到RST时,不必发ACK确认,就立刻关闭连接
发送RST的时机:
- 建立连接的
SYN
到达某个端口,但是该端口没有监听的服务 - 目的主机或者网络路径中防火墙拦截
- 请求超时。 使用setsockopt的
SO_RCVTIMEO
选项设置recv
的超时时间。接收数据超时时,会发送RST包 - socket的数据未完全读取完,就执行关闭
- 向已经关闭的socket发送数据
- 向已关闭的连接发送FIN
- 向已经消逝的连接中发送数据,已经消逝是指内核已经不在维护这个连接的状态,数据结构已经在内核中注销
RST与broken pipe
Unix系统中,对已经关闭的socket或者管道等进行操作,会出现SIGPIPE
信号,因此如果网络连接中出现了broken pipe
类似的错误,一般是对已经关闭的连接进行了操作,此时需要检测连接的活性。
RST与connect reset by peer
这种情况出现在,A向B发送FIN准备关闭连接,B发送ACK之后,网络出现了问题;等网络恢复之后,A却因为某些原因重启等,总之就是A丢失了之前B发来的ACk;如果此时B继续发送新的包,则A无法识别这些包,会发送给B一个RST
,那么B就会出现connect reset by peer
的错误,此时A是强制关闭连接。
RST攻击
假设A和B正常发送数据,那么RST攻击就是想办法让A或B其中一方异常地发送RST
。比如,C伪造了TCP端,在A和B正常通信时,向其中一方发送SYN
;或者直接向其中一方发送RST
包。
伪造的方式是模拟源端口号和序列号。一般来说,通过OS来随机发送序列号,而且不要让对方找到对应的规律即可。
TCP状态转换过程
再给出TCP状态转换图
MSL
:Maximum Segment Lifetime
每个报文分组在网络上存活的最长生命周期。一般是ttl的限制。
网络中的异常情况:
- 分组重复发送:一个分组可能有些特殊原因,造成了超时发送,此时发送方会重复发送数据,重复发的数据正常到达,之后旧的数据
服务器维护的命令
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT和CLOSWE_WAIT状态:
先给出两种状态的出现情况:
TIME_WAIT是主动关闭连接的一方进入的状态。4次握手的时候,我们不能保证以下两点:
- 主动关闭方的最后的
ACK
可以正常到达 - 网络上没有残余的数据包
因此,主动关闭的一方,需要一定的时间,来保证上述两点的完成。比如,网络原因使得被动关闭方没有收到主动关闭方的ACK
,那么此时会重新发送FIN
,但是如果主动关闭方早就发送完ACK
,然后关闭了连接,那么被动发送方就会收到RST
,造成会话出错的现象,但实际上会话是正常结束的,因此此时需要这个状态,来防备这种错误。
同样的,如果发送方的socket
关闭,此时新建立连接,恰好使用了同样的端口,而网络中还有残留的数据没有发送完毕,则现在的数据可能是之前的报文,因此TIME_WAIT
内,不允许复用现有的连接。
一般来说,TIME_WAIT
需要2*MSL
的时间。如果同时出现大量的TIME_WAIT
状态,可以通过有关的配置调节时间。这种状态是消耗fd的。
CLOSE_WAIT
状态:
该状态没有过期时间,一般来说,需要我们强制关闭连接等解决。
连接的本质和socket
OSI7层模型:
- 物理层
- 数据链路层
- 网路层
- 运输层
- 会话层
- 表示层
- 应用层
socket
:套接字,这是与OS内核的TCP、UDP和IP等操作的一个系统调用API。socket
不能确切地说位于哪一层,而是作为不同层通信的接口。比如会话层可以通过scoket
控制运输层和网络层等。
在C/S等结构中,每个连接可以用一个五元组表示{src_ip,, src_port, protocol, dist_ip, dist_port}
,源(地址+端口) + 协议 + 目的(地址+端口)。从机器的视角来看,连接必然要占用资源,Linux中一切皆文件,因此用一个文件描述符(fd, file description)来表示这个连接资源;那么一个socket
可以认为提供了对这个fd读写的统一方法。
之后,需要明白,服务器的port和socket
本身没有直接的关系。理论上,假设机器可以开辟无限个fd,那么这个机器的某个端口可以和世界上所有的机器建立连接。但实际上机器可以打开的fd是有限制,利用ulimit -n
可以获取最大的fd的个数。即fd的数量限制了实际可以创建的连接的个数。在这里,我们忽略了OS转换不同的连接数据到不同socket的过程。
之后,理解长连接和短连接的区别:
- 客户端的角度:
- 短连接:TCP3次握手服务器建立连接 --> 传输一个批次数据 --> 4次挥手关闭连接 --> 回收本地的
socket
资源。新的传输,重新创建连接。 - 长连接:TCP3次握手 --> 多次利用同一个
socket
传输数据 --> 4次挥手关闭连接
- 短连接:TCP3次握手服务器建立连接 --> 传输一个批次数据 --> 4次挥手关闭连接 --> 回收本地的
- 服务器的角度:
- 短连接:TCP3次握手建立与客户端的通信 --> 接收客户端一个批次的通信 --> 执行4次挥手 --> 关闭
socket
的fd。如果客户端没有主动关闭,服务器也会自动关闭fd - 长连接:TCP3次握手建立与客户端的通信 --> 接收客户端一个批次的通信 --> 如果客户端主动关闭,则关闭本地的fd;否则一直等待。
- 短连接:TCP3次握手建立与客户端的通信 --> 接收客户端一个批次的通信 --> 执行4次挥手 --> 关闭
从上面的描述可以看出,长连接和短连接的本质区别是:socekt
的fd是否在一次通信后关闭
长连接的优势在于每次不需要新建socket
,即不需要新的fd,复用之前的即可,那么建立会话和传输数据的开销会变小;缺点在于:fd的个数是有上限的,不能无限创建。一般来说,服务器的fd会成为限制。因此,像http
等协议都是短连接的。
网络连接中,会设置超时时间,超时时间本质上是socket
的关闭时间。比如服务器给每个连接的时间是10s,那么如果10s没收到客户端的消息,则主动关闭对应的socket
的fd,此时这个连接就失效了。长连接设置超时时间,是为了防止资源耗尽,假设超时时间是无限的,那么客户端长时间连接不上服务器,则新建新的连接,这样对服务器来说,又新建了一个scoket
,而之前的那个会永远的失去意义,因此造成资源泄露。
IO复用
IO复用与多进程和多线程没有必然关系。IO复用是指,在一次轮询中,检测到多个有IO事件fd,并处理这些fd的IO请求。处理这些fd请求的过程,才涉及到多进程(线程)。以epoll为例子:
#define MAX_EVENTS 1000
int main() {
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/* Code to set up listening socket, 'listen_sock',
(socket(), bind(), listen()) omitted */
epollfd = epoll_create1(0);
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
// 永久阻塞,直到有事件
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) { // 处理错误
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) { // 错误码
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);
ev.events = EPOLLIN | EPOLLET; // 设置为边沿触发
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { // 新建连接
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd); // 已经存在的fd的IO事件,这里可以多进程(线程)处理,但是是在IO复用完成之后了
}
}
}
return 0;
}
上述的epoll是典型的rector模型,即有IO事件后,立刻执行处理。而proacotr模式,是先进行IO,然后再通知处理函数。而系统没提供proacotr,一般是我们自己实现封装,绑定回调函数。
网络编程错误
read broken pipe
:这表示IO操作遇到了RST
,可以参考RST
的内容。EOF
:对接收到FIN的socket
进行读操作,因为此时FIN
携带EOF
数据
参考资料
- https://blog.youkuaiyun.com/qq_35976351/article/details/85228002
- https://learnku.com/articles/6485/understanding-and-distinguishing-tcp-http-socket-ports-connections-concurrency-and-qps
- https://zhuanlan.zhihu.com/p/53685973
- https://blog.youkuaiyun.com/guowenyan001/article/details/11766929
- https://zhuanlan.zhihu.com/p/30791159
- https://en.wikipedia.org/wiki/Transmission_Control_Protocol
- https://www.jianshu.com/p/394cafc91d18