Linux网络编程04:网络基础(万字图文解析)
04-网络基础
本文主要用于补充部分计算网络理论知识及Linux相关网络命令,应用层的HTTP协议与HTTPS请阅读Linux网络编程03:HTTP与HTTPS),更多计算机网络理论知识请阅读本人的计算机网络专栏。
一、传输层补充
1、端口号与bind
1)一个进程是否可以 bind 多个端口号?
✅ 可以。 一个进程可以同时绑定(bind)多个不同的端口号,甚至可以绑定多个 IP 地址 + 端口的组合。 在 TCP/UDP 协议中,端口号(Port Number)用于区分不同的服务或应用程序。操作系统允许一个进程创建多个 socket套接字,每个 socket 可以绑定 不同的 IP + 端口组合。端口只是“标识符”,进程可以同时监听多个端口,就像一个公司可以有多个客服电话(不同分机号)。
2)一个端口号是否可以被多个进程 bind?
❌ 不可以。 一个端口号(IP + 端口组合)在同一时间只能被一个进程绑定(bind()
),否则会报错。这是由操作系统网络栈(TCP/IP 协议栈)的设计原理决定的,操作系统需要明确“谁来处理这个连接”,例如:当客户端访问服务器时,它会发送一个IP + 端口的数据包,这时操作系统必须决定这个数据包应该交给哪个进程处理。如果多个进程都绑定了同一个端口,操作系统就不知道该把连接交给谁,会导致冲突。
2、命令:netstat
netstat是一个用来查看网络状态的重要工具。
- 常用选项:
n 拒绝显示别名,能显示数字的全部转化成数字
l 仅列出有在 Listen (监听) 的服务状态
p 显示建立相关链接的程序名
t (tcp)仅显示tcp相关选项
u (udp)仅显示udp相关选项
a (all)显示所有选项,默认不显示LISTEN相关
- 显示udp相关选项
[root@VM-8-16-centos Notes]# netstat -nuap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
udp 0 0 0.0.0.0:68 0.0.0.0:* 1008/dhclient
udp 0 0 10.1.8.16:123 0.0.0.0:* 699/ntpd
udp 0 0 127.0.0.1:123 0.0.0.0:* 699/ntpd
udp6 0 0 fe80::5054:ff:feb9::123 :::* 699/ntpd
udp6 0 0 ::1:123 :::* 699/ntpd
- 显示tcp相关选项
[root@VM-8-16-centos Notes]# netstat -nltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:33673 0.0.0.0:* LISTEN 19030/node
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1320/sshd
tcp6 0 0 :::3306 :::* LISTEN 19675/mysqld
tcp6 0 0 :::22 :::* LISTEN 1320/sshd
tcp6 0 0 :::33060 :::* LISTEN 19675/mysqld
- 如果没有使用n则会显示别名
[root@VM-8-16-centos Notes]# netstat -ltp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 VM-8-16-centos:33673 0.0.0.0:* LISTEN 19030/node
tcp 0 0 0.0.0.0:ssh 0.0.0.0:* LISTEN 1320/sshd
tcp6 0 0 [::]:mysql [::]:* LISTEN 19675/mysqld
tcp6 0 0 [::]:ssh [::]:* LISTEN 1320/sshd
tcp6 0 0 [::]:33060 [::]:* LISTEN 19675/mysqld
3、命令:pidof
- 通过进程名查看服务器的进程id
[shenalex@VM-8-16-centos tmp_code]$ ./tcpserver 8080
[shenalex@VM-8-16-centos tmp_code]$ pidof tcpserver
24211
使用xargs将pidof的结果作为kill命令参数从而杀死进程:
[shenalex@VM-8-16-centos tmp_code]$ pidof tcpserver | xargs kill -9
4、命令:ifconfig
ipconfig命令(Windows版本是ipconfig)可以展示本地网卡的信息,其中ens33(有些系统是使用eth0)是表示以太网网卡,而lo表示loopback本地回环设备。
[alex@localhost ~]$ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500inet 192.168.10.129 netmask 255.255.255.0 broadcast 192.168.10.255inet6 fe80::5139:aedb:9988:f9d1 prefixlen 64 scopeid 0x20<link>ether 00:0c:29:81:b5:1c txqueuelen 1000 (Ethernet)RX packets 73 bytes 24665 (24.0 KiB)RX errors 0 dropped 0 overruns 0 frame 0TX packets 80 bytes 9741 (9.5 KiB)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536inet 127.0.0.1 netmask 255.0.0.0inet6 ::1 prefixlen 128 scopeid 0x10<host>loop txqueuelen 1000 (Local Loopback)RX packets 64 bytes 5568 (5.4 KiB)RX errors 0 dropped 0 overruns 0 frame 0TX packets 64 bytes 5568 (5.4 KiB)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
5、协议报头是一种结构化对象
在网络协议中,报头(header)通常被定义为结构化的数据对象,以便于在代码中进行处理和解析。例如,udp协议中的报头,操作系统内核可能将其定义为一个结构体:
struct udp_header
{uint16_t src_port;uint16_t dsc_port;uint16_t length;uint16_t check;
}; // 注:非操作系统内核真实定义,只是用于举例从而便于理解
6、拷贝接口与内核缓冲区
我们之前在网络套接字中说明的诸如sendto, recvfrom, read, write, send, recv等IO接口实际上都不是真正的发送接口,而是拷贝接口。例如客户端要发送数据给服务器,在使用send()
系统调用时是将用户层所需要发送的数据拷贝到内核的发送缓冲区而不是直接就发送到服务器了,实际上什么时候才将内核发送缓冲区的数据发送给服务器这一过程是由操作系统控制的。服务器接收到的来自客户端的数据也不是直接就交给用户层使用,而是先保存在接收缓冲区,当调用read()
系统调用时才把接收缓冲区数据拷贝到用户层使用。
TCP协议是面向连接的可靠传输协议,一个主机的进程向另一主机进程发送数据后,需要由对方发送ACK确认,故而当前主机从发送缓冲区发送出去的数据对应的缓冲区位置不会被新数据立即覆盖,而是等待收到确认ACK后对应位置的缓冲区才会被新数据使用。
7、UDP协议的缓冲区
- UDP没有真正意义上的发送缓冲区。调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作;
- UDP具有接收缓冲区。但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再到达的UDP数据就会被丢弃。
8、TCP协议
1)URG、PSH、RST字段含义
-
RST强制终止TCP连接的异常状态。当RST=1时,表示发送方要求立即释放连接,无需等待数据传输完成或正常挥手流程。
-
URG用于标记报文段中包含紧急数据,需优先处理。当URG=1时,紧急指针字段(16 bit)生效,指向紧急数据的最后一个字节。
-
PSH用于提示接收方立即将数据交付应用层,而非等待缓冲区填满。当PSH=1时,发送方要求接收方“推送”当前缓冲区中的所有数据(包括本报文数据)至应用层。
2)使用1、2次握手可不可行?
如果使用1次握手,意味着客户端向服务端发起链接请求被马上接受,此时二者开始通信,且不论是否服务端数据能否正常发送数据给客户端,此方式可能会导致严重的SYN洪水问题。
使用2次握手也是不可行的,因为服务端发起的SYN未确认。一种典型的场景就是客户端发起SYN,第一个SYN超时并重传,第二个SYN到达并建立连接,之后再完成连接并关闭,倘若关闭之后,第一个SYN到达服务端,此时服务端就会认为对方建立连接,并回复SYN+ACK,由于没有确认,所以服务端并不知道客户端的状态,此时客户端完全可能已经关闭,那服务端就会陷入永久等待了。
3)TIME_WAIT状态
先尝试启动一个tcp服务器和客户端
[shenalex@VM-8-16-centos tmp_code]$ make
g++ -o tcpserver tcpServer.cc -std=c++11 -lpthread
g++ -o tcpclient tcpClient.cc -std=c++11
[shenalex@VM-8-16-centos tmp_code]$ ./tcpserver 8080
Thread-1 is running...
Thread-2 is running...
Thread-3 is running...
Thread-4 is running...
Thread-5 is running...
[shenalex@VM-8-16-centos tmp_code]$ ./tcpclient 127.0.0.1 8080
Enter#
然后使用ctrl+c组合键关闭服务端,然后马上再尝试重新启动服务器端,发现启动失败了
这是因为TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,需要等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口;
[shenalex@VM-8-16-centos tmp_code]$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
- 服务器端的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的:
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)。
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接。由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议)。这样服务端的工作能力会极大地受到限制,而取消TIME_WAIT状态其实对可靠性的影响比较小,所以用户可以选择使用
setsockopt
函数修改监听套接字的属性,使其可以在TIME_WAIT状态下依然可以 bind 重复的地址(需要在 bind 之前执行)。
- 使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
_listensock = socket(AF_INET, SOCK_STREAM, 0);// ...int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 后续再执行bind等...if (bind(_listensock, reinterpret_cast<const sockaddr *>(&local), sizeof(local)) == -1)
{// ...
}
// ...
4)粘包问题
在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。站在应用层的角度, 看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
想要避免粘包问题,我们必须明确两个包之间的边界。
对于定长的包,保证每次都按固定大小读取即可;
对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是用户写代码的时候自己来定的,只要保证分隔符不和正文冲突即可);
UDP协议是否也存在 “粘包问题” ?
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层,有很明确的数据边界。站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么没有收到,因而不存在“粘包问题”。
5)TCP异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN和正常关闭没有什么区别。
- 机器重启:同进程终止。
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。 如果对方不在,也会把连接释放。
- 另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。
6)全链接与半链接队列
- listen的第二个参数:
int listen(int sockfd, int backlog);
服务端在开启了 listen 之后,就可以开始接受客户端连接了。一旦启用了 listen 之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发送和接收缓冲区,转而在内核区维护两个队列结构:半连接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)和全连接队列(用来保存处于established状态,但是应用层没有调用accept取走的请求)。半连接队列用于管理成功第一次握手的连接,全连接队列用于管理已经完成三次握手的队列。 backlog 在有些操作系统用来指明半连接队列和全连接队列的长度之和,一般填一个正数即可。如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复RST,以方便客户端自动重传),即无法继续让当前连接的状态进入established状态了。这个队列的长度是listen的第二个参数backlog + 1。
测试:
将服务器的listen的第二个参数设置为1,并且不调用accept。
当我们启动前面的两个客户端进程时,发现一切正常(注:无关的tcp链接已经略去),State为ESTABLISHED:
当我们启动第三个客户端时,发现它是SYS_RECV
状态:
- DDOS攻击
利用半连接队列的设计思路,网络攻击者想到了一种恶意攻击的方法。他们伪造一些SYN请求但是并不打算建立连接,这些请求的源地址随机构建的,或者是感染其他计算机(即“肉鸡”)来发起请求,服务端内核就会维持一个很大的队列来管理这些半连接。当半连接足够多的时候,就会导致新来的正常连接请求得不到响应, 也就是所谓的DDOS攻击。一般的防御措施就是就是减小SYN+ACK重传次数、增加半连接队列长度、启用syn cookie。不过在高强度攻击面前,调整tcp_syn_retries 和 tcp_max_syn_backlog并不能解决根本问题。更有效的防御手段是激活tcp_syncookies——在连接真正创建起来之前,它并不会立刻给请求分配数据区存储连接状态,而是通过构建一个带签名的序号来屏蔽伪造请求。
9、命令:telnet
Telnet 是一个基于 TCP 的网络协议和命令行工具,主要用于远程登录和网络服务调试。
telnet [主机名或IP地址] [端口号]
退出telnet:
- 按下
Ctrl + ]
(先按住 Ctrl 键,再按右方括号键]
)。这会进入 Telnet 命令模式,提示符变为telnet>
。 - 输入
quit
或close
,然后按回车,即可完全退出 Telnet。
示例:
[shenalex@VM-8-16-centos tmp_code]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
^]
telnet> q
Connection closed.
二、数据链路层补充
1、ifconfig查看mtu
MTU是以太网最大传输单元,以太网帧中的数据长度规定最小46字节,最大1500字节。使用ifconfig可以查看mtu
2、命令:arp
ARP命令用于查看和管理系统ARP缓存。
命令功能 | 常用命令示例 | 说明 |
---|---|---|
📋 显示所有 ARP 表项 | arp -a | 显示所有接口上的 ARP 缓存条目 |
🔢 以数字形式显示 (不解析域名) | arp -n | 直接显示 IP 地址,而非尝试 DNS 解析主机名 |
📝 显示详细信息 | arp -v | 显示 ARP 缓存条目的详细信息 |
[alex@localhost ~]$ arp -a
? (192.168.10.254) at 00:50:56:fb:4b:80 [ether] on ens33
gateway (192.168.10.2) at 00:50:56:e5:0e:0a [ether] on ens33
本文仅用于补充部分计算网络理论知识及Linux相关网络命令,更多计算机网络理论知识请阅读本人的计算机网络专栏。
参考资料
- 谢希仁. (2024). 计算机网络 (第8版). 北京: 电子工业出版社.
- 游双. (2013). Linux高性能服务器编程. 北京: 机械工业出版社.
- 王道计算机教育. (2019). 计算机网络课程[B站视频]. Bilibili. https://www.bilibili.com/video/BV19E411D78Q?vd_source=33ca4a4964785165eb751135aadccddd
- 湖科大教书匠. (2019). 计算机网络微课堂[B站视频]. Bilibili. https://www.bilibili.com/video/BV1c4411d7jb?vd_source=33ca4a4964785165eb751135aadccddd