计算机网络总结

计算机网络基础

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状态转换图

MSLMaximum 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次挥手 --> 关闭socket的fd。如果客户端没有主动关闭,服务器也会自动关闭fd
    • 长连接:TCP3次握手建立与客户端的通信 --> 接收客户端一个批次的通信 --> 如果客户端主动关闭,则关闭本地的fd;否则一直等待。

从上面的描述可以看出,长连接和短连接的本质区别是: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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值