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_RCVBUFSO_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_fdclient_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连接(如发送或接收数据)。

总结流程

  1. socket() : 创建一个struct sock(具体为struct tcp_sock),并与socket_fd关联。

  2. listen() : 在tcp_sock上创建半连接队列和全连接队列:

    • 半连接队列存放struct request_sock,用于管理三次握手中的连接。
    • 全连接队列存放已建立连接的struct tcp_sock
  3. 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

以下是一些常用的标志及其作用,它们可以组合使用(使用按位或操作符 |):

  1. MSG_DONTWAIT

    • 使send()成为非阻塞操作。如果套接字的发送缓冲区已满,send()不会等待缓冲区空闲,而是立即返回,返回值为-1,并设置errnoEAGAINEWOULDBLOCK
    • 适用于非阻塞套接字,也可以临时使阻塞套接字表现为非阻塞模式。
  2. MSG_OOB(Out-of-Band Data):

    • 发送紧急数据(带外数据),仅适用于TCP协议。紧急数据会优先于普通数据处理,但在实际应用中,带外数据的使用较少。
    • 常用于一些需要快速响应的特殊场景。
  3. MSG_NOSIGNAL

    • 如果向已断开的连接发送数据,通常会触发SIGPIPE信号,导致程序终止。使用该标志可以抑制SIGPIPE信号,防止程序崩溃。
    • 适用于需要处理网络中断且不希望信号干扰的场景。
  4. MSG_CONFIRM

    • 仅适用于基于某些协议(如UDP)的发送,表示希望确认对端的存在,通常用于实现链路层的邻居确认。
    • 仅用于某些低层协议的特定场景,在常规TCP/UDP应用中较少使用。
  5. MSG_DONTROUTE

    • 发送数据时,不查找路由表,直接将数据发送到与目标网络直接相连的接口。通常用于网络诊断和本地网络通信的场景。
    • 在大多数普通应用场景中很少使用。
  6. MSG_EOR(End of Record):

    • 仅用于某些基于记录的协议,表示本次send()调用发送的数据是一个逻辑记录的结束。
    • 对于常见的TCP或UDP通信,这个标志不常用。
  7. MSG_MORE

    • 表示应用程序还有更多的数据要发送。在某些协议(如TCP)中,使用该标志时,内核会暂时将数据保留在缓冲区中,而不是立即发送,以减少网络上的包数。
    • 适合分多次发送数据,但希望减少网络开销的场景。

示例:使用 MSG_DONTWAITMSG_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协议的三次握手四次挥手 过程中,虽然在三次握手时可以将SYNACK合并到一个数据包发送,但在四次挥手过程中,FINACK通常不能合并到同一个数据包发送。这主要与TCP的连接状态和双方通信的半关闭状态有关。

1. 三次握手(SYN 和 ACK 合并的原因)

在三次握手中,通信双方需要同步序列号,建立可靠的连接。具体过程是:

  • 第一次握手 :客户端发送一个SYN包,表示请求建立连接,并传递初始序列号。
  • 第二次握手 :服务器收到SYN后,回复一个包含SYNACK的包。这里的ACK是对客户端SYN的确认,而SYN则是服务器请求建立连接的信号。因为SYNACK是针对不同的动作(SYN是服务器发起的,而ACK是对客户端请求的确认),可以一起合并发送。
  • 第三次握手 :客户端收到后,发送ACK确认,连接建立。

这里之所以可以合并,是因为双方的状态在逻辑上是同步的,服务器既要发出自己的SYN,又要确认客户端的SYN,可以一起处理。

2. 四次挥手(ACK 和 FIN 通常不能合并的原因)

四次挥手过程用于关闭TCP连接,具体如下:

  • 第一次挥手 :客户端发送一个FIN包,表示它要关闭连接(数据传输结束)。
  • 第二次挥手 :服务器收到后,回复一个ACK,表示收到了客户端的FIN请求,但服务器可能还在发送数据。
  • 第三次挥手 :服务器发送完数据后,再发送一个FIN包,表示它也同意关闭连接。
  • 第四次挥手 :客户端收到服务器的FIN后,发送一个ACK包,确认关闭。
原因:
  1. 连接的半关闭状态 : 在四次挥手过程中,TCP协议允许连接进入半关闭状态,即:

    • 当客户端发送FIN请求时,意味着客户端已经不再发送数据,但服务器还可以继续发送未完成的数据。
    • 客户端发送的FIN和服务器接收的ACK是两个不同的操作,它们代表了不同的状态。

    在这个阶段,服务器回复的ACK只是表明收到了客户端的FIN,但服务器还没有准备好关闭连接,因为可能仍然有数据需要发送。如果此时合并ACKFIN,就意味着服务器已经准备好关闭连接了,但实际上它可能还没有完成数据发送。

  2. 不同的时间点ACKFIN通常不会在同一时刻发生:

    • 客户端发FIN后,服务器需要立即回复一个ACK,但是服务器可能还在发送数据,并未准备好关闭连接。
    • 只有当服务器确认所有数据发送完毕后,它才会发送FIN来关闭连接。这两个操作通常在不同的时间点发生,无法合并。
  3. 确保数据完整性 : 在四次挥手中,分开ACKFIN的发送有助于确保所有数据都能成功传输完毕。服务器通过先发送ACK确认收到客户端的关闭请求,并在数据发送完毕后才发送FIN,可以避免数据丢失或中途终止传输。

3. 总结

  • 在三次握手中,SYNACK可以合并到一个数据包中发送,因为它们在逻辑上是并行的操作,且是在同一时刻发送的。
  • 在四次挥手中,ACKFIN不能合并发送,因为它们通常发生在不同的时间点,表示不同的状态转换。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_RDSHUT_WR

2. FIN报文的发送

shutdown()函数的how参数为SHUT_WRSHUT_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 动态端口范围,用于临时分配给客户端

端口号的使用说明

  1. 给用户的端口号:这些端口号由操作系统和服务程序为用户提供,用来执行应用程序或服务的访问。这些端口号一般需要符合特定协议,使用时需要确保没有冲突。

    • 例如:Web 服务使用 80 或 443 端口,邮件服务使用 25、110 或 143 端口。
  2. 给协议的端口号:协议端口号由 IANA(Internet Assigned Numbers Authority)分配,用于区分不同的网络协议和服务。许多常见的协议和服务有固定的端口号,比如 HTTP(80)、FTP(21)、SSH(22)等。

  3. 特定协议的端口号:许多协议和应用程序会规定固定的端口号,用于指定特定的服务。例如:

    • HTTP/HTTPS 协议默认使用端口 80 和 443。
    • FTP 使用端口 21 进行控制连接,端口 20 用于数据连接。
    • SMTP 使用端口 25 发送邮件,POP3 使用端口 110 接收邮件。
  4. 动态分配端口:客户端与服务器建立连接时,通常会使用动态端口(范围 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 是一种面向连接、可靠的数据传输协议,它保证数据的可靠交付,确保所有数据包按照顺序到达目的地,并通过流量控制、拥塞控制等机制避免网络过载。为确保这些特性,某些操作需要等待特定事件的发生,导致阻塞:

  1. 等待数据到达 :如在调用 recv() 时,系统必须等待数据从远程主机传输到本地。
  2. 等待连接建立 :如在 accept()connect() 中,系统必须等待对方准备好接收或发起连接。
  3. 缓冲区未满:如在发送数据时,如果发送缓冲区已满,系统会等待缓冲区腾出空间来进行数据传输。

如何避免阻塞?

  1. 非阻塞模式 :可以将套接字设置为非阻塞模式,在这种模式下,调用函数不会阻塞。如果操作无法立即完成,它会返回 EAGAINEWOULDBLOCK 错误,应用程序可以做其他事情或稍后再试。

    • 代码示例

      int flags = fcntl(sockfd, F_GETFL, 0);
      fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);  // 设置套接字为非阻塞模式
      
  2. 超时设置 :对于 connect()recv() 等函数,可以设置超时时间,避免长时间阻塞。比如可以使用 select()poll() 来实现超时检测。

  3. 多线程或异步 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时表示对方已经断开连接。

  • EAGAINEWOULDBLOCK: 套接字被设置为非阻塞模式,且没有数据可用时返回该错误。
  • EBADF: 套接字无效,可能是已经关闭或未正确初始化。
  • EINTR : 系统调用被信号中断。操作在信号处理程序执行后被中断,导致 recv() 返回失败。
  • ENOTCONN : 套接字未连接,调用 recv() 时,TCP 套接字未完成连接。
  • ECONNRESET: 对方主机强制关闭连接,TCP 连接被重置,导致接收操作失败。
  • ENOTSOCK: 目标文件描述符不是一个套接字。
  • EFAULT: 提供的缓冲区地址无效。

3. send() / sendto() / write()

这些函数用于向 TCP 套接字发送数据,失败时返回 -1,并设置 errno

  • EAGAINEWOULDBLOCK: 套接字被设置为非阻塞模式,且发送缓冲区已满,无法继续发送数据。
  • 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 包 来终止连接),并且没有更多的数据可接收。
    • 这个返回值表示对方已经优雅地关闭了连接,并且没有数据需要读取。

具体情况:

  1. TCP 连接正常关闭

    • 在正常的 TCP 连接关闭过程中,通信双方会经过四次挥手(Four-way Handshake),具体来说:
      • 一方(通常是主动关闭的那方)发送一个 FIN 包,表示希望关闭连接。
      • 接收方确认收到 FIN 包 ,并发送一个 ACK 包
      • 接收方也会发送自己的 FIN 包,表示自己也准备关闭连接。
      • 主动关闭的一方确认收到接收方的 FIN 包,完成连接关闭过程。

    在这个过程中,当 recv() 读取到接收到的 FIN 包 后,表示对方已经关闭了连接,函数返回 0

  2. 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: 系统调用被信号中断。
      • EAGAINEWOULDBLOCK: 套接字处于非阻塞模式,且没有数据可读取。
      • ECONNRESET: 连接被对方重置(例如,远程主机强制关闭了连接)。
  • 返回大于零的正数

    • 如果返回一个大于 0 的值,表示成功接收到数据,值表示接收到的数据字节数。开发者可以处理这些数据。

为什么 recv() 返回 0 代表对方关闭了连接?

这是因为在 TCP 协议中,连接关闭是通过发送 FIN 包来实现的。此时,连接的另一端会通知接收端自己已经没有数据发送,并且希望关闭连接。当接收端收到这个 FIN 包后,recv() 返回 0,表示没有更多的数据可读。

  • TCP FIN 包 :当连接的某一方发送 FIN 包时,它表示已经发送完所有数据并且希望关闭连接。接收方接收到 FIN 包后,会回复一个 ACK 包 ,表示已经收到关闭请求。此时,接收方的接收缓冲区为空,不再有数据传输,recv() 将返回 0,表示对方已关闭连接。

总结:

  • recv() 返回 0 表示对方已经关闭了连接,通常是正常的连接关闭过程。
  • 该返回值是用于 优雅地关闭连接 的指示,表明没有更多数据可读,开发者可以清理资源并关闭自己的套接字。
  • 当遇到 0 时,通常需要进行关闭套接字、清理资源等操作。

0voice · GitHub

相关推荐
qq_433618448 分钟前
shell 编程(五)
linux·运维·服务器
VVVVWeiYee39 分钟前
项目2路由交换
运维·服务器·网络·网络协议·信息与通信
小伍_Five2 小时前
透视网络世界:计算机网络习题的深度解析与总结【前3章】
服务器·网络·计算机网络
芷栀夏3 小时前
如何在任何地方随时使用本地Jupyter Notebook无需公网IP
服务器·ide·tcp/ip·jupyter·ip
广而不精zhu小白3 小时前
CentOS Stream 9 挂载Windows共享FTP文件夹
linux·windows·centos
一休哥助手3 小时前
全面解析 Linux 系统监控与性能优化
linux·运维·性能优化
二进制杯莫停3 小时前
掌控网络流量的利器:tcconfig
linux
watl04 小时前
【Android】unzip aar删除冲突classes再zip
android·linux·运维
网络安全(king)4 小时前
网络安全攻防学习平台 - 基础关
网络·学习·web安全
赵大仁4 小时前
在 CentOS 7 上安装 Node.js 20 并升级 GCC、make 和 glibc
linux·运维·服务器·ide·ubuntu·centos·计算机基础