Linux高级--2.4.2 linux TCP 系列操作函数 -- 深层理解
一、操作函数简介
在 Linux 中,TCP(传输控制协议)操作涉及多种系统调用和函数,通常用来创建套接字、连接、发送/接收数据、关闭连接等。以下是一些常用的 TCP 操作函数和它们的简要说明:
1. socket()
- 函数原型:
int socket(int domain, int type, int protocol);
- 功能: 创建一个新的套接字(socket),它是与网络通信相关的基本对象。
- 参数:
domain
: 协议族(如AF_INET
用于 IPv4,AF_INET6
用于 IPv6)。type
: 套接字类型(如SOCK_STREAM
表示 TCP,SOCK_DGRAM
表示 UDP)。protocol
: 使用的协议,通常设为0
,由系统自动选择合适的协议。
- 返回值: 返回一个套接字描述符(文件描述符),失败时返回
-1
。
2. bind()
- 函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 将套接字与本地地址(IP 地址和端口)绑定。
- 参数:
sockfd
: 要绑定的套接字。addr
: 地址结构,通常是struct sockaddr_in
,指定 IP 和端口。addrlen
: 地址结构的长度。
- 返回值: 成功返回
0
,失败返回-1
。
3. listen()
- 函数原型:
int listen(int sockfd, int backlog);
- 功能: 将套接字设置为被动模式,等待客户端连接。
- 参数:
sockfd
: 套接字描述符。backlog
: 最多可连接的等待队列的大小。
- 返回值: 成功返回
0
,失败返回-1
。
4. accept()
- 函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能: 接受来自客户端的连接请求,并返回一个新的套接字描述符用于与客户端通信。
- 参数:
sockfd
: 已经调用listen()
的套接字。addr
: 客户端的地址信息。addrlen
: 地址结构的大小。
- 返回值: 返回新的套接字描述符,用于与客户端的通信,失败时返回
-1
。
5. connect()
- 函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端发起与服务器的连接请求。
- 参数:
sockfd
: 客户端套接字描述符。addr
: 目标服务器的地址信息。addrlen
: 地址结构的长度。
- 返回值: 成功返回
0
,失败返回-1
。
6. send()
- 函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 功能: 通过套接字发送数据。
- 参数:
sockfd
: 套接字描述符。buf
: 数据缓冲区。len
: 发送数据的长度。flags
: 发送标志(一般设为0
)。
- 返回值: 返回实际发送的字节数,失败时返回
-1
。
7. recv()
- 函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能: 从套接字接收数据。
- 参数:
sockfd
: 套接字描述符。buf
: 存储接收到数据的缓冲区。len
: 接收数据的最大长度。flags
: 接收标志(一般设为0
)。
- 返回值: 返回实际接收的字节数,失败时返回
-1
。
8. close()
- 函数原型:
int close(int fd);
- 功能: 关闭套接字,释放相关资源。
- 参数:
fd
: 套接字描述符。
- 返回值: 成功返回
0
,失败返回-1
。
9. shutdown()
- 函数原型:
int shutdown(int sockfd, int how);
- 功能: 用于关闭套接字的读、写或者双向通信。
- 参数:
sockfd
: 套接字描述符。how
: 控制关闭的方式,常用值为:SHUT_RD
: 关闭读取(不能再读取数据)。SHUT_WR
: 关闭写入(不能再发送数据)。SHUT_RDWR
: 同时关闭读写。
- 返回值: 成功返回
0
,失败返回-1
。
10. getsockopt() 和 setsockopt()
- 函数原型:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- 功能: 用于获取或设置套接字的选项(如 TCP 的各种参数,如缓冲区大小、超时时间等)。
- 参数:
sockfd
: 套接字描述符。level
: 设置选项的协议层级,通常为SOL_SOCKET
(套接字层)或IPPROTO_TCP
(TCP 层)。optname
: 选项名称(如SO_RCVBUF
,SO_RCVBUF
等)。optval
: 选项的值。optlen
: 选项值的长度。
- 返回值: 成功返回
0
,失败返回-1
。
11. select() 和 poll()
- 函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 功能: 允许程序监听多个套接字,并在某些事件(如可读、可写等)发生时进行处理。
12. accept4()(Linux 特有)
- 函数原型:
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
- 功能: 与
accept()
类似,但支持额外的标志(如SOCK_NONBLOCK
等),在非阻塞模式下返回。 - 返回值: 返回一个新的套接字描述符,失败时返回
-1
。
小结:
这些是常见的用于 TCP 通信的 Linux 系统调用和函数。它们允许应用程序通过网络进行基本的连接管理、数据发送/接收等操作。通常情况下,服务器会使用 socket()
、bind()
、listen()
和 accept()
来创建并处理客户端连接,而客户端则使用 socket()
和 connect()
发起连接。数据的发送和接收使用 send()
和 recv()
。
二、socket/listen/accept与TCB的关系
下面将详细解释在socket()
、listen()
、accept()
等函数调用过程中,TCP控制块(TCB,struct tcp_sock
)的创建和队列的使用,以及它们与文件描述符(socket_fd
和 client_fd
)的关系。
1. socket()
函数调用后的TCB关联
- 当你调用
socket()
函数时,操作系统会为这个套接字创建一个struct sock
结构体(具体来说,如果是TCP套接字,将创建一个struct tcp_sock
,它是struct sock
的子类)。这个结构体就是TCP控制块(TCB),负责管理该套接字的所有TCP连接状态。 - 创建的
sock
结构体会与socket_fd
绑定,socket_fd
是应用层与内核层进行通信的文件描述符。通过socket_fd
,内核可以找到与之关联的sock
结构体。
2. listen()
函数调用后的队列创建
- 当调用
listen()
函数时,TCP进入监听状态,这时在与该监听套接字对应的TCB上会创建两个队列:- 半连接队列(Syn Queue):存放正在进行三次握手的连接。
- 全连接队列(Accept Queue):存放已经完成三次握手的连接。
这些队列用于管理TCP连接的不同状态,但队列中的成员并不是直接的TCB(struct tcp_sock
)类型:
-
半连接队列中的成员:是
struct request_sock
类型。request_sock
是一个轻量级的数据结构,用于在三次握手未完成时存储连接请求的状态信息。在接收到客户端的SYN之后,服务端在半连接队列中分配一个request_sock
,并等待三次握手完成。 -
全连接队列中的成员:在三次握手完成后,内核会从半连接队列移除
request_sock
并创建一个完整的struct tcp_sock
(也称作TCB),然后将其移入全连接队列中,表示该连接已经建立。
3. accept()
函数调用后
-
当应用程序调用
accept()
函数时,内核会从全连接队列中取出一个已经完成三次握手的TCP连接。 -
在全连接队列中的成员是一个完整的
struct tcp_sock
(即TCB),它记录了该连接的所有TCP状态。 -
内核会为这个新的TCP连接创建一个新的文件描述符,称为
client_fd
,并将该文件描述符与这个TCP连接的TCB(struct tcp_sock
)进行绑定。换句话说,
client_fd
与新连接的struct tcp_sock
关联起来,使得通过client_fd
可以操作该TCP连接(如发送或接收数据)。
总结流程
-
socket()
: 创建一个struct sock
(具体为struct tcp_sock
),并与socket_fd
关联。 -
listen()
: 在tcp_sock
上创建半连接队列和全连接队列:- 半连接队列存放
struct request_sock
,用于管理三次握手中的连接。 - 全连接队列存放已建立连接的
struct tcp_sock
。
- 半连接队列存放
-
accept()
: 从全连接队列中取出一个struct tcp_sock
,为它分配一个新的文件描述符client_fd
,并将client_fd
与这个TCP连接的TCB(struct tcp_sock
)绑定。
因此,调用accept()
后,全连接队列中的TCP连接会与新的client_fd
关联,应用程序通过client_fd
来处理这个TCP连接。
三、listen函数backlog的作用
listen()
函数的backlog
参数在TCP服务器中用于指定全连接队列(Accept Queue)的最大长度,即允许在服务器上排队等待accept()
的已建立连接的最大数量。
1. listen()
函数及 backlog
参数的作用
当你调用listen()
函数时,服务器的套接字进入监听状态,开始等待客户端的连接请求。backlog
参数定义了以下内容:
- 最大已完成连接数:
backlog
参数指定全连接队列的最大长度,即已经完成三次握手但尚未被应用程序accept()
取走的连接数。 - 当客户端发起连接请求并完成了三次握手,连接会被放入全连接队列。如果队列已满,新完成的连接将被拒绝,客户端会收到TCP RST(复位)信号,表示连接无法建立。
2. backlog
参数的工作机制
在listen(sockfd, backlog)
中:
- 全连接队列(Accept Queue) 存放的是已经完成三次握手、处于
ESTABLISHED
状态的连接,这些连接等待应用程序调用accept()
来处理。 - 半连接队列(Syn Queue) 管理尚未完全建立的连接(正在三次握手中的连接),它与
backlog
关系较小,主要受tcp_max_syn_backlog
内核参数的影响。
具体行为:
- 当全连接队列中的连接数达到
backlog
限制时,新完成的连接将无法进入队列,导致客户端收到RST包,连接被拒绝。 - 如果设置的
backlog
值太小,服务器可能无法处理高并发连接,导致连接请求频繁被拒绝。 - 如果设置的
backlog
值过大,可能会增加系统负担,尤其是在没有足够的资源或处理能力时。
3. backlog
参数的实际值
-
虽然应用程序可以指定
backlog
的大小,但内核实际上会对该值进行限制。 -
Linux内核中有一个参数
somaxconn
,它定义了允许的最大backlog
值。如果你在listen()
中传入的backlog
值大于/proc/sys/net/core/somaxconn
中设定的值,系统会将backlog
限制为somaxconn
的值。- 查看和调整
somaxconn
参数:cat /proc/sys/net/core/somaxconn echo 1024 > /proc/sys/net/core/somaxconn
- 查看和调整
4. 实际例子
假设你调用了如下的listen()
函数:
listen(sockfd, 10);
- 这意味着全连接队列的长度最大为10,即最多允许10个已经完成三次握手的连接排队等待
accept()
。 - 如果第11个连接尝试建立,服务器将返回TCP RST包,拒绝该连接。
5. 总结
backlog
参数用于指定服务器上全连接队列的最大长度,即等待应用层accept()
调用的已建立连接数的最大值。- 过小的
backlog
值会导致高并发时连接被拒绝,而过大的值会增加系统资源占用,需根据系统处理能力合理设置。
四、半连接队列的限制
在 TCP 服务器中,半连接队列的数量(即 SYN 队列)由内核的 tcp_max_syn_backlog
参数控制。
1. 半连接队列(SYN队列):
- 当客户端向服务器发送 SYN 请求时,服务器将这个连接请求放入 半连接队列(也称为 SYN 队列)。此队列用于存储尚未完成三次握手的连接。
- 一旦握手完成并且服务器准备好接受数据,连接就会移入 全连接队列(Accept Queue)。
2. tcp_max_syn_backlog
参数:
- 作用: 控制半连接队列的最大长度,即可以缓存的未完成三次握手的连接数。
- 默认值: 在大多数 Linux 系统中,默认值通常为 128,意味着最多可以缓存 128 个尚未完成三次握手的连接。
- 调整: 可以通过修改
/proc/sys/net/ipv4/tcp_max_syn_backlog
文件来调整此值。例如:
或者在echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
sysctl.conf
中添加:net.ipv4.tcp_max_syn_backlog=2048
3. SYN 队列溢出:
- 如果半连接队列已满并且有新的 SYN 请求到达,内核会丢弃这些连接请求,通常客户端会收到一个 TCP RST(重置) 消息,或者如果客户端重试,可能会延迟连接。
- 为了避免此情况,通常需要根据实际的网络负载来调整该参数,尤其是在高并发的服务器上。
4. 全连接队列:
- 在调用
listen()
函数时,backlog
参数设置的是 全连接队列 的大小,即已完成三次握手的连接的最大数量。它并不直接影响半连接队列的大小。 - 如果 全连接队列 已满,
accept()
会阻塞,直到队列中有空间为止。
总结:
- 半连接队列(SYN 队列)的大小是由
tcp_max_syn_backlog
参数控制。 - 全连接队列(Accept Queue)的大小是由
listen()
函数的backlog
参数控制。
因此,半连接队列和全连接队列的长度由不同的参数控制,而服务器需要根据实际的负载情况合理配置这些参数,以确保高并发时的连接性能和稳定性。
五、send函数的第四个参数是什么作用
send()
函数的第四个参数是**flags
**,用于指定发送操作的行为。通过设置不同的标志,应用程序可以控制send()
函数的具体行为。
send()
函数的原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd
:目标套接字的文件描述符。buf
:要发送的数据的缓冲区。len
:要发送的数据长度。flags
:控制发送行为的标志位(即第四个参数)。
常用的 flags
值
以下是一些常用的标志及其作用,它们可以组合使用(使用按位或操作符 |
):
-
MSG_DONTWAIT
:- 使
send()
成为非阻塞操作。如果套接字的发送缓冲区已满,send()
不会等待缓冲区空闲,而是立即返回,返回值为-1
,并设置errno
为EAGAIN
或EWOULDBLOCK
。 - 适用于非阻塞套接字,也可以临时使阻塞套接字表现为非阻塞模式。
- 使
-
MSG_OOB
(Out-of-Band Data):- 发送紧急数据(带外数据),仅适用于TCP协议。紧急数据会优先于普通数据处理,但在实际应用中,带外数据的使用较少。
- 常用于一些需要快速响应的特殊场景。
-
MSG_NOSIGNAL
:- 如果向已断开的连接发送数据,通常会触发
SIGPIPE
信号,导致程序终止。使用该标志可以抑制SIGPIPE
信号,防止程序崩溃。 - 适用于需要处理网络中断且不希望信号干扰的场景。
- 如果向已断开的连接发送数据,通常会触发
-
MSG_CONFIRM
:- 仅适用于基于某些协议(如UDP)的发送,表示希望确认对端的存在,通常用于实现链路层的邻居确认。
- 仅用于某些低层协议的特定场景,在常规TCP/UDP应用中较少使用。
-
MSG_DONTROUTE
:- 发送数据时,不查找路由表,直接将数据发送到与目标网络直接相连的接口。通常用于网络诊断和本地网络通信的场景。
- 在大多数普通应用场景中很少使用。
-
MSG_EOR
(End of Record):- 仅用于某些基于记录的协议,表示本次
send()
调用发送的数据是一个逻辑记录的结束。 - 对于常见的TCP或UDP通信,这个标志不常用。
- 仅用于某些基于记录的协议,表示本次
-
MSG_MORE
:- 表示应用程序还有更多的数据要发送。在某些协议(如TCP)中,使用该标志时,内核会暂时将数据保留在缓冲区中,而不是立即发送,以减少网络上的包数。
- 适合分多次发送数据,但希望减少网络开销的场景。
示例:使用 MSG_DONTWAIT
和 MSG_NOSIGNAL
char message[] = "Hello, World!";
int result = send(sockfd, message, sizeof(message), MSG_DONTWAIT | MSG_NOSIGNAL);
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区已满,发送失败
printf("Send would block, try again later.\n");
} else {
// 处理其他错误
perror("send");
}
}
总结
send()
函数的第四个参数flags
用于控制发送操作的行为。常见的标志包括MSG_DONTWAIT
(非阻塞发送)、MSG_OOB
(发送紧急数据)、MSG_NOSIGNAL
(避免SIGPIPE
信号)等。你可以根据具体应用场景使用不同的标志来改变send()
的默认行为。
六、为什么握手要三次,挥手要四次,挥手中间的两次不能像握手那样合并在一起吗?
在TCP协议的三次握手和四次挥手过程中,虽然在三次握手时可以将SYN
和ACK
合并到一个数据包发送,但在四次挥手过程中,FIN
和ACK
通常不能合并到同一个数据包发送。这主要与TCP的连接状态和双方通信的半关闭状态有关。
1. 三次握手(SYN 和 ACK 合并的原因)
在三次握手中,通信双方需要同步序列号,建立可靠的连接。具体过程是:
- 第一次握手:客户端发送一个
SYN
包,表示请求建立连接,并传递初始序列号。 - 第二次握手:服务器收到
SYN
后,回复一个包含SYN
和ACK
的包。这里的ACK
是对客户端SYN
的确认,而SYN
则是服务器请求建立连接的信号。因为SYN
和ACK
是针对不同的动作(SYN
是服务器发起的,而ACK
是对客户端请求的确认),可以一起合并发送。 - 第三次握手:客户端收到后,发送
ACK
确认,连接建立。
这里之所以可以合并,是因为双方的状态在逻辑上是同步的,服务器既要发出自己的SYN
,又要确认客户端的SYN
,可以一起处理。
2. 四次挥手(ACK 和 FIN 通常不能合并的原因)
四次挥手过程用于关闭TCP连接,具体如下:
- 第一次挥手:客户端发送一个
FIN
包,表示它要关闭连接(数据传输结束)。 - 第二次挥手:服务器收到后,回复一个
ACK
,表示收到了客户端的FIN
请求,但服务器可能还在发送数据。 - 第三次挥手:服务器发送完数据后,再发送一个
FIN
包,表示它也同意关闭连接。 - 第四次挥手:客户端收到服务器的
FIN
后,发送一个ACK
包,确认关闭。
原因:
-
连接的半关闭状态: 在四次挥手过程中,TCP协议允许连接进入半关闭状态,即:
- 当客户端发送
FIN
请求时,意味着客户端已经不再发送数据,但服务器还可以继续发送未完成的数据。 - 客户端发送的
FIN
和服务器接收的ACK
是两个不同的操作,它们代表了不同的状态。
在这个阶段,服务器回复的
ACK
只是表明收到了客户端的FIN
,但服务器还没有准备好关闭连接,因为可能仍然有数据需要发送。如果此时合并ACK
和FIN
,就意味着服务器已经准备好关闭连接了,但实际上它可能还没有完成数据发送。 - 当客户端发送
-
不同的时间点:
ACK
和FIN
通常不会在同一时刻发生:- 客户端发
FIN
后,服务器需要立即回复一个ACK
,但是服务器可能还在发送数据,并未准备好关闭连接。 - 只有当服务器确认所有数据发送完毕后,它才会发送
FIN
来关闭连接。这两个操作通常在不同的时间点发生,无法合并。
- 客户端发
-
确保数据完整性: 在四次挥手中,分开
ACK
和FIN
的发送有助于确保所有数据都能成功传输完毕。服务器通过先发送ACK
确认收到客户端的关闭请求,并在数据发送完毕后才发送FIN
,可以避免数据丢失或中途终止传输。
3. 总结
- 在三次握手中,
SYN
和ACK
可以合并到一个数据包中发送,因为它们在逻辑上是并行的操作,且是在同一时刻发送的。 - 在四次挥手中,
ACK
和FIN
不能合并发送,因为它们通常发生在不同的时间点,表示不同的状态转换。ACK
是对接收方收到FIN
的确认,而FIN
是表示发送方准备完全关闭连接,这两者之间可能存在数据传输的延迟,因此分开发送有助于确保传输的可靠性和完整性。
4. close/shutdown与挥手报文的关系:
当recv函数返回0时表示收到了对方的FIN报文,此时close()调用后,会直接发出ACK + FIN。
但是用shutdown(sockfd, SHUT_RD)后只会发出 ACK,不会给发出FIN,还可以接着给对方发送数据。
七、shutdown函数 与 FIN 报文
是的,调用shutdown()
函数时,根据调用参数,TCP连接可以发送FIN报文,但这取决于shutdown()
的具体使用方式。
1. shutdown()
函数的作用
shutdown()
函数用于部分或完全关闭一个已经建立的TCP连接。它不同于close()
函数,close()
不仅会关闭连接,还会释放文件描述符,而shutdown()
允许程序在不关闭文件描述符的情况下关闭连接的某一方向(发送或接收)。
shutdown()
函数的原型:
int shutdown(int sockfd, int how);
其中:
sockfd
:要关闭的套接字描述符。how
:决定关闭连接的方式。其值可以是以下之一:SHUT_RD (0)
:关闭接收方向,该套接字不再能接收数据。SHUT_WR (1)
:关闭发送方向,该套接字不再能发送数据,并发送FIN包。SHUT_RDWR (2)
:同时关闭发送和接收方向,等同于分别调用SHUT_RD
和SHUT_WR
。
2. FIN报文的发送
当shutdown()
函数的how
参数为SHUT_WR
或SHUT_RDWR
时,TCP协议会发送一个FIN报文,告诉对方主机发送方已经关闭,数据发送已完成,表明不会再有更多的数据从该端发送。
详细说明:
-
SHUT_WR (1)
:关闭发送方向。当调用shutdown(sockfd, SHUT_WR)
时,TCP协议栈会发送一个FIN报文,表示发送端不再发送数据。之后,这一端仍然可以接收对方的数据,但不能再发送任何数据。 -
SHUT_RDWR (2)
:同时关闭发送和接收方向。调用shutdown(sockfd, SHUT_RDWR)
时,发送FIN,且无法再接收对方的数据。此时,连接相当于完全关闭,但文件描述符不会被释放,应用程序仍然可以继续使用文件描述符做其他操作。
3. shutdown()
与close()
的区别
shutdown()
函数可以只关闭连接的一部分(如只关闭发送而保留接收),而close()
会完全关闭连接并释放套接字文件描述符。- 在调用
close()
时,如果还有数据没有发送完,TCP协议栈会继续尝试发送剩余数据,并最终发送FIN报文,完成四次挥手流程。
4. 典型使用场景
-
shutdown(sockfd, SHUT_WR)
:当一个应用程序完成了发送数据,但仍然希望接收对方的数据时,通常会调用这个函数。例如,HTTP协议中,服务器发送响应数据后可能会调用shutdown()
来关闭发送方向,但仍然保留接收方向以读取客户端的请求。 -
shutdown(sockfd, SHUT_RDWR)
:用于完全关闭连接,类似于close()
,但不释放文件描述符。
5. 总结
当调用shutdown(sockfd, SHUT_WR)
或shutdown(sockfd, SHUT_RDWR)
时,TCP会发送FIN报文,表示发送方已经完成数据传输,关闭了发送方向。
八、bind函数端口号的设置
端口号在网络协议中起着非常重要的作用,它们被用来标识不同的服务或应用程序。端口号可以分为两大类:知名端口(Well-Known Ports)和动态或私有端口(Dynamic or Private Ports)。这些端口号由 Internet Assigned Numbers Authority (IANA) 管理,确保网络中的每个服务都能有唯一的端口标识。
端口号的分类
- 知名端口(Well-Known Ports): 范围为 0 到 1023,通常分配给操作系统和知名的服务协议。
- 注册端口(Registered Ports): 范围为 1024 到 49151,供用户和应用程序使用。
- 动态或私有端口(Dynamic or Private Ports): 范围为 49152 到 65535,通常用于临时分配给客户端应用。
知名端口(0 - 1023)
这些端口通常由 IANA 分配给常用服务和协议,以下是一些常见的协议和对应的端口号:
端口号 | 协议 / 服务 | 说明 |
---|---|---|
20 | FTP 数据传输(File Transfer Protocol) | 用于 FTP 数据传输 |
21 | FTP 控制(File Transfer Protocol) | 用于 FTP 控制连接 |
22 | SSH(Secure Shell) | 用于安全远程登录 |
23 | Telnet | 用于非加密的远程登录 |
25 | SMTP(Simple Mail Transfer Protocol) | 用于邮件传输 |
53 | DNS(Domain Name System) | 用于域名解析 |
67 | DHCP 服务器端(Dynamic Host Configuration Protocol) | 用于 DHCP 服务器 |
68 | DHCP 客户端(Dynamic Host Configuration Protocol) | 用于 DHCP 客户端 |
69 | TFTP(Trivial File Transfer Protocol) | 用于轻量级的文件传输 |
80 | HTTP(HyperText Transfer Protocol) | 用于 Web 服务(网页浏览) |
110 | POP3(Post Office Protocol version 3) | 用于邮件接收 |
119 | NNTP(Network News Transfer Protocol) | 用于新闻组协议 |
123 | NTP(Network Time Protocol) | 用于网络时间同步 |
143 | IMAP(Internet Message Access Protocol) | 用于邮件接收(替代 POP3) |
161 | SNMP(Simple Network Management Protocol) | 用于网络设备管理 |
194 | IRC(Internet Relay Chat) | 用于即时聊天 |
443 | HTTPS(HyperText Transfer Protocol Secure) | 用于加密的 Web 服务(HTTPS) |
514 | Syslog | 用于网络设备和操作系统的日志记录 |
520 | RIP(Routing Information Protocol) | 用于路由协议 |
3389 | RDP(Remote Desktop Protocol) | 用于远程桌面访问 |
注册端口(1024 - 49151)
这些端口主要由软件供应商和开发者为其应用程序所使用。IANA 对这些端口进行注册,但它们通常不属于标准化的、固定的协议。以下是一些常见的服务和对应的端口号:
端口号 | 协议 / 服务 | 说明 |
---|---|---|
1080 | SOCKS(SOCKS Proxy Protocol) | 用于代理服务 |
1433 | Microsoft SQL Server | 用于 Microsoft SQL 数据库服务 |
3306 | MySQL | 用于 MySQL 数据库服务 |
3389 | Microsoft RDP | 用于远程桌面协议 |
5432 | PostgreSQL | 用于 PostgreSQL 数据库服务 |
5900 | VNC(Virtual Network Computing) | 用于虚拟网络计算(远程桌面控制) |
8080 | HTTP(Alternative Port) | 用于 Web 服务的备用端口(HTTP) |
8888 | HTTP(Alternative Port) | 用于 Web 服务的备用端口(HTTP) |
动态或私有端口(49152 - 65535)
这些端口通常由操作系统或应用程序动态分配给客户端程序使用,尤其是在进行临时连接时。它们不固定分配给任何特定服务。通常在 TCP/IP 会话中,客户端通过使用这些端口号连接到远程服务器的服务端口。
端口号范围 | 说明 |
---|---|
49152 - 65535 | 动态端口范围,用于临时分配给客户端 |
端口号的使用说明
-
给用户的端口号:这些端口号由操作系统和服务程序为用户提供,用来执行应用程序或服务的访问。这些端口号一般需要符合特定协议,使用时需要确保没有冲突。
- 例如:Web 服务使用 80 或 443 端口,邮件服务使用 25、110 或 143 端口。
-
给协议的端口号:协议端口号由 IANA(Internet Assigned Numbers Authority)分配,用于区分不同的网络协议和服务。许多常见的协议和服务有固定的端口号,比如 HTTP(80)、FTP(21)、SSH(22)等。
-
特定协议的端口号:许多协议和应用程序会规定固定的端口号,用于指定特定的服务。例如:
- HTTP/HTTPS 协议默认使用端口 80 和 443。
- FTP 使用端口 21 进行控制连接,端口 20 用于数据连接。
- SMTP 使用端口 25 发送邮件,POP3 使用端口 110 接收邮件。
-
动态分配端口:客户端与服务器建立连接时,通常会使用动态端口(范围 49152 到 65535)。例如,在 HTTP 请求中,客户端使用随机分配的端口号连接服务器的端口 80 或 443。
端口号的重要性
- 网络服务和协议标识:端口号帮助操作系统区分不同的网络协议和服务,使得同一台机器可以同时提供多个不同的服务。
- 安全性考虑:某些服务使用的端口号可能存在安全漏洞,因此安全防护设备(如防火墙)通常会对端口号进行过滤,阻止不安全的端口。
- 端口扫描:攻击者通常通过端口扫描来查找开放的端口和运行的服务,进而寻找潜在的攻击入口。
总结
- 端口号是网络通信中的重要组成部分,允许不同的服务和应用程序在同一台机器上并行运行。
- 端口号分为知名端口、注册端口和动态端口,分别用于系统服务、应用程序服务和临时连接。
- 各种协议和服务使用不同的端口号,IANA 负责管理这些端口号的分配。
九、TCP系统调用函数的 -- 阻塞性
在 TCP 编程中,某些网络操作可能会阻塞,即函数在没有完成操作之前会等待特定条件的发生。这些函数通常用于执行需要等待数据到达、连接建立、或者连接关闭等操作的任务。阻塞行为通常与网络状态、系统资源、以及协议本身的特性相关。
以下是一些常见的会阻塞的 TCP 相关函数,以及它们为什么会阻塞:
1. accept()
- 阻塞原因:
accept()
用于在服务器端接受一个已经完成三次握手的连接请求。如果没有等待的连接,它会阻塞,直到有客户端发起连接请求。 - 何时阻塞: 当没有客户端连接请求到达时,
accept()
会阻塞,直到有连接请求到来。 - 代码示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(sockfd, 5); int client_sock = accept(sockfd, NULL, NULL); // 阻塞直到有连接到来 if (client_sock == -1) { perror("accept"); }
2. recv()
/ recvfrom()
/ read()
- 阻塞原因: 这些函数用于从套接字中接收数据。如果没有数据可读,它们会阻塞,直到有数据可用。
recv()
在默认情况下会阻塞,直到接收到至少一个字节的数据。 - 何时阻塞: 如果缓冲区中没有数据(例如,客户端没有发送数据),则会阻塞等待数据的到来。
- 代码示例:
char buffer[1024]; int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0); // 阻塞直到数据到达 if (bytes_received == -1) { perror("recv"); }
3. send()
/ sendto()
/ write()
- 阻塞原因: 如果发送缓冲区已满,
send()
或write()
可能会阻塞,直到发送缓冲区有足够的空间来存储数据。特别是在网络拥堵或者接收方的速度跟不上发送速度时,发送函数可能会阻塞。 - 何时阻塞: 当套接字处于阻塞模式且发送缓冲区已满时,
send()
或write()
会阻塞,直到缓冲区有空间。 - 代码示例:
const char *msg = "Hello, Client!"; int bytes_sent = send(client_sock, msg, strlen(msg), 0); // 阻塞直到数据被发送 if (bytes_sent == -1) { perror("send"); }
4. connect()
- 阻塞原因:
connect()
用于客户端与服务器建立 TCP 连接。如果服务器没有响应或不可达,connect()
会阻塞,直到连接成功建立或者超时。 - 何时阻塞: 如果没有可用的远程服务器响应或服务器未准备好接收连接,
connect()
会阻塞,直到连接成功或失败。 - 代码示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); server_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); int result = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); // 阻塞直到连接成功 if (result == -1) { perror("connect"); }
5. listen()
- 阻塞原因:
listen()
是在 TCP 服务器端调用的,用于将套接字设为监听模式,等待客户端的连接请求。它本身不会阻塞,但会准备好接收连接。在后续调用accept()
时,才会阻塞。 - 何时阻塞:
listen()
本身不会阻塞,但它为accept()
阻塞操作做好准备。 - 代码示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(sockfd, 5); // 为后续的accept准备
6. shutdown()
- 阻塞原因:
shutdown()
可以关闭套接字的某些操作(如读、写),并等待数据的完全传输或清理。如果你调用shutdown()
来关闭写操作,它可能会阻塞,直到 TCP 将所有待发送的数据发送完毕。 - 何时阻塞: 如果套接字有未发送的数据需要传输,
shutdown()
会阻塞,直到数据传输完毕。 - 代码示例:
int result = shutdown(sockfd, SHUT_WR); // 关闭写端,阻塞直到所有数据被发送 if (result == -1) { perror("shutdown"); }
为什么阻塞?
TCP 是一种面向连接、可靠的数据传输协议,它保证数据的可靠交付,确保所有数据包按照顺序到达目的地,并通过流量控制、拥塞控制等机制避免网络过载。为确保这些特性,某些操作需要等待特定事件的发生,导致阻塞:
- 等待数据到达:如在调用
recv()
时,系统必须等待数据从远程主机传输到本地。 - 等待连接建立:如在
accept()
和connect()
中,系统必须等待对方准备好接收或发起连接。 - 缓冲区未满:如在发送数据时,如果发送缓冲区已满,系统会等待缓冲区腾出空间来进行数据传输。
如何避免阻塞?
-
非阻塞模式:可以将套接字设置为非阻塞模式,在这种模式下,调用函数不会阻塞。如果操作无法立即完成,它会返回
EAGAIN
或EWOULDBLOCK
错误,应用程序可以做其他事情或稍后再试。- 代码示例:
int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); // 设置套接字为非阻塞模式
- 代码示例:
-
超时设置:对于
connect()
、recv()
等函数,可以设置超时时间,避免长时间阻塞。比如可以使用select()
或poll()
来实现超时检测。 -
多线程或异步 I/O:在多线程程序中,阻塞的操作可以放在单独的线程中执行,主线程继续进行其他任务。使用异步 I/O 也是一种避免阻塞的方式,特别是在高性能网络应用中。
总结
TCP 编程中,许多函数会阻塞,尤其是与连接、数据接收和发送相关的操作。accept()
、recv()
、send()
和 connect()
等函数在没有数据或连接不可用时会阻塞,直到特定条件满足。为了避免阻塞,开发者可以使用非阻塞模式、超时机制或多线程来处理阻塞操作。
十、调用返回失败的情况分析
在 TCP 编程中,许多常用的函数会返回失败的情况,尤其是在网络条件不理想或系统资源不足时。了解每个函数返回失败时的具体错误原因非常重要,这有助于调试和错误处理。以下是对常见的 TCP 函数返回失败时的错误情况的详细说明:
1. accept()
accept()
用于接受客户端连接。当调用失败时,返回值为 -1
,并设置 errno
以指示具体的错误原因。
EINVAL
: 如果套接字未正确绑定(如bind()
未调用)或者套接字类型不支持accept()
(例如,UDP 套接字),则返回此错误。ECONNABORTED
: 如果先前的连接被中止,accept()
返回此错误。EFAULT
: 传递给accept()
的地址指针无效。EINTR
: 系统调用被信号中断。accept()
被中断时返回该错误。
2. recv()
/ recvfrom()
/ read()
这些函数用于从 TCP 套接字接收数据。当返回 -1
时,表示出现错误,errno
将设置为相应的错误代码。0时表示对方已经断开连接。
EAGAIN
或EWOULDBLOCK
: 套接字被设置为非阻塞模式,且没有数据可用时返回该错误。EBADF
: 套接字无效,可能是已经关闭或未正确初始化。EINTR
: 系统调用被信号中断。操作在信号处理程序执行后被中断,导致recv()
返回失败。ENOTCONN
: 套接字未连接,调用recv()
时,TCP 套接字未完成连接。ECONNRESET
: 对方主机强制关闭连接,TCP 连接被重置,导致接收操作失败。ENOTSOCK
: 目标文件描述符不是一个套接字。EFAULT
: 提供的缓冲区地址无效。
3. send()
/ sendto()
/ write()
这些函数用于向 TCP 套接字发送数据,失败时返回 -1
,并设置 errno
。
EAGAIN
或EWOULDBLOCK
: 套接字被设置为非阻塞模式,且发送缓冲区已满,无法继续发送数据。EBADF
: 套接字无效,可能是已经关闭或未正确初始化。EINTR
: 系统调用被信号中断,导致send()
被中断。ENOTCONN
: 套接字未连接时调用send()
会失败。ECONNRESET
: 对方主机强制关闭连接,导致连接重置,发送操作失败。ENOTSOCK
: 目标文件描述符不是一个套接字。EPIPE
: 当发送数据到一个已经关闭的连接时返回此错误,表示对方已经关闭了连接,写入操作失败。
4. connect()
connect()
用于客户端建立与服务器的连接。如果返回值是 -1
,则表示连接失败,errno
会被设置为特定错误值。
ECONNREFUSED
: 目标服务器拒绝连接。通常是目标服务器未启动或未监听指定的端口。ETIMEDOUT
: 连接请求超时。在指定时间内没有完成连接。EINPROGRESS
: 如果套接字是非阻塞模式且连接正在进行中,这个错误会发生。不是错误,表示连接正在进行。EAGAIN
: 套接字设置为非阻塞模式时,连接尝试会立即返回EAGAIN
错误,表示无法立即连接。EADDRINUSE
: 本地地址已在使用中,无法为新连接分配。ENETUNREACH
: 网络不可达,可能是由于路由或网络配置问题。EHOSTUNREACH
: 主机不可达,通常由于目标主机未开机或网络不可达。ENOTSOCK
: 目标文件描述符不是一个套接字。
5. listen()
listen()
用于在服务器端启动监听。失败时返回 -1
,并设置 errno
。
EADDRINUSE
: 如果指定的端口已被其他应用程序占用,listen()
会失败并返回此错误。EINVAL
: 如果套接字不是流式套接字(例如 UDP 套接字),则会发生此错误。ENOTSOCK
: 传入的文件描述符不是套接字。
6. shutdown()
shutdown()
用于关闭套接字的读写操作。如果返回 -1
,则表示操作失败,errno
被设置为错误值。
EBADF
: 套接字无效,可能是已经关闭或者未正确初始化。EINTR
: 系统调用被信号中断,shutdown()
被中断。ENOTSOCK
: 目标文件描述符不是一个套接字。
7. fcntl()
fcntl()
用于获取或设置套接字的属性,如设置非阻塞模式等。如果返回 -1
,表示操作失败,errno
被设置为错误码。
EBADF
: 套接字无效,可能是已经关闭或未正确初始化。EINVAL
: 无效的命令或参数。ENOTTY
: 非法的文件描述符类型,不支持该操作。
8. bind()
bind()
用于将套接字与本地地址(IP 和端口)绑定。如果返回 -1
,表示绑定失败,errno
被设置为特定错误码。
EADDRINUSE
: 地址已被使用,无法绑定。EADDRNOTAVAIL
: 本地地址不可用,可能由于没有该网络接口或地址配置问题。EBADF
: 套接字无效。EINVAL
: 无效的套接字类型,通常是由于套接字类型和协议不匹配。
总结
了解这些 TCP 函数返回失败时的错误原因非常重要,有助于调试和错误处理。一般情况下,当函数返回 -1
时,errno
会提供失败的详细信息。开发者应该根据不同的错误代码进行适当的错误处理,例如通过重试、记录日志、关闭套接字等方式来恢复网络操作,确保程序的健壮性。
十一、recv返回0时的详细说明
在 TCP 编程中,recv()
函数的返回值为 0
是一个非常重要的情况,它表示 对方关闭了连接。这个情况常常被用来判断连接是否已经正常关闭。
recv()
返回0
:- 当调用
recv()
时,如果返回值是0
,这并不表示错误,而是表示连接已经被对方关闭(也就是对方发送了一个 TCP FIN 包 来终止连接),并且没有更多的数据可接收。 - 这个返回值表示对方已经优雅地关闭了连接,并且没有数据需要读取。
- 当调用
具体情况:
-
TCP 连接正常关闭:
- 在正常的 TCP 连接关闭过程中,通信双方会经过四次挥手(Four-way Handshake),具体来说:
- 一方(通常是主动关闭的那方)发送一个 FIN 包,表示希望关闭连接。
- 接收方确认收到 FIN 包,并发送一个 ACK 包。
- 接收方也会发送自己的 FIN 包,表示自己也准备关闭连接。
- 主动关闭的一方确认收到接收方的 FIN 包,完成连接关闭过程。
在这个过程中,当
recv()
读取到接收到的 FIN 包 后,表示对方已经关闭了连接,函数返回0
。 - 在正常的 TCP 连接关闭过程中,通信双方会经过四次挥手(Four-way Handshake),具体来说:
-
recv()
返回0
的例子: 下面是一个简单的代码示例,演示如何使用recv()
判断对方关闭连接:char buffer[1024]; int bytes_received; // 假设客户端已经连接到服务器 bytes_received = recv(client_sock, buffer, sizeof(buffer), 0); if (bytes_received == 0) { printf("The remote side has closed the connection gracefully.\n"); // 对方关闭了连接,进行相应的清理工作 close(client_sock); } else if (bytes_received < 0) { perror("recv failed"); } else { // 处理接收到的数据 printf("Received %d bytes: %s\n", bytes_received, buffer); }
在上面的代码中:
- 如果
recv()
返回0
,表示对方已经正常关闭了连接。此时,通常需要关闭自己的套接字并清理相关资源。 - 如果
recv()
返回负值(< 0
),表示发生了错误,可以通过errno
获取错误原因。
- 如果
其他返回情况:
-
返回负值(
< 0
):- 如果
recv()
返回一个负值,通常表示发生了错误。常见的错误包括:EINTR
: 系统调用被信号中断。EAGAIN
或EWOULDBLOCK
: 套接字处于非阻塞模式,且没有数据可读取。ECONNRESET
: 连接被对方重置(例如,远程主机强制关闭了连接)。
- 如果
-
返回大于零的正数:
- 如果返回一个大于
0
的值,表示成功接收到数据,值表示接收到的数据字节数。开发者可以处理这些数据。
- 如果返回一个大于
为什么 recv()
返回 0
代表对方关闭了连接?
这是因为在 TCP 协议中,连接关闭是通过发送 FIN 包来实现的。此时,连接的另一端会通知接收端自己已经没有数据发送,并且希望关闭连接。当接收端收到这个 FIN 包后,recv()
返回 0
,表示没有更多的数据可读。
- TCP FIN 包:当连接的某一方发送
FIN
包时,它表示已经发送完所有数据并且希望关闭连接。接收方接收到FIN
包后,会回复一个 ACK 包,表示已经收到关闭请求。此时,接收方的接收缓冲区为空,不再有数据传输,recv()
将返回0
,表示对方已关闭连接。
总结:
recv()
返回0
表示对方已经关闭了连接,通常是正常的连接关闭过程。- 该返回值是用于 优雅地关闭连接 的指示,表明没有更多数据可读,开发者可以清理资源并关闭自己的套接字。
- 当遇到
0
时,通常需要进行关闭套接字、清理资源等操作。
0voice · GitHub