• 从本质上来讲,所谓的建立连接,其实是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,并用这样的数据结构来保证面向连接的特性。TCP 无法左右中间的任何通路,也没有什么虚拟的连接,中间的通路根本意识不到两端使用了 TCP 还是 UDP

  • 所谓的连接,就是两端数据结构状态的协同,两边的状态能够对得上。符合 TCP 协议的规则,就认为连接存在两面状态对不上,连接就算断了

  • 流量控制和拥塞控制, 其实就是根据收到的对端的网络包,调整两端数据结构的状态。TCP 协议的设计理论上认为,这样调整了数据结构的状态,就能进行流量控制和拥塞控制了,其实在通路上是不是真的做到了,谁也管不着

  • 所谓的可靠,也是两端的数据结构做的事情。不丢失其实是数据结构在“点名”,顺序到达其实是数据结构在“排序”,面向数据流其实是数据结构将零散的包,按照顺序捏成一个流发给应用层。总而言之,“连接”两个字让人误以为功夫在通路,其实功夫在两端

当然,无论是用 socket 操作 TCP,还是 UDP,我们首先都要调用 socket 函数:

  1. int socket(int domain, int type, int protocol);

socket 函数用于创建一个 socket 的文件描述符,唯一标识一个 socket。我们把它叫作文件描述符.

socket 函数有三个参数:

  • domain:表示使用什么 IP 层协议。AF_INET 表示 IPv4,AF_INET6 表示 IPv6。(选择网络层协议)
  • type:表示 socket 类型。SOCK_STREAM,顾名思义就是 TCP 面向流的,SOCK_DGRAM 就是 UDP 面向数据报的,SOCK_RAW 可以直接操作 IP 层,或者非 TCP 和 UDP 的协议。例如 ICMP。(选择传输层协议)
  • protocol 表示的协议,包括 IPPROTO_TCP、IPPTOTO_UDP。

通信结束后,我们还要像关闭文件一样,关闭 socket。

针对 TCP 应该如何编程?

image.png

TCP 的服务端要先监听一个端口,一般是先调用 bind 函数,给这个 socket 赋予一个端口和 IP 地址。

  1. int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  2. struct sockaddr_in {
  3. __kernel_sa_family_t sin_family; /* Address family */
  4. __be16 sin_port; /* Port number */
  5. struct in_addr sin_addr; /* Internet address */
  6. /* Pad to size of `struct sockaddr'. */
  7. unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
  8. sizeof(unsigned short int) - sizeof(struct in_addr)];
  9. };
  10. struct in_addr {
  11. __be32 s_addr;
  12. };
  • sockfd 是上面我们创建的 socket 文件描述符。
  • 在 sockaddr_in 结构中
    • sin_family 设置为 AF_INET,表示 IPv4;
    • sin_port 是端口号;
    • sin_addr 是 IP 地址。

如果在网络上传输超过 1 Byte 的类型,就要区分大端(Big Endian)和小端(Little Endian)。

接下来,服务端要调用 listen 进入 LISTEN 状态,等待客户端进行连接 (将会三次握手)

  1. int listen(int sockfd, int backlog);

三次握手:

image.png

接着,服务端只需要调用 accept,等待内核完成了至少一个连接的建立,才返回。

  • 如果没有一个连接完成了三次握手,accept 就一直等待;
  • 如果有多个客户端发起连接,并且在内核里面完成了多个三次握手,建立了多个连接,这些连接会被放在一个队列里面。accept 会从队列里面取出一个来进行处理。
  • 如果想进一步处理其他连接,需要调用多次 accept,所以 accept 往往在一个循环里面。
  1. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接下来,客户端可以通过 connect 函数发起连接:

  1. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

我们先在参数中指明要连接的 IP 地址和端口号,然后发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 socket

这里需要注意的是,监听的 socket 和真正用来传送数据的 socket,是两个 socket,一个叫作监听 socket,一个叫作已连接 socket。成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

针对 UDP 应该如何编程?

image.png

UDP 是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因而也需要 bind。

对于 UDP 来讲,没有所谓的连接维护,也没有所谓的连接的发起方和接收方,甚至都不存在客户端和服务端的概念,大家就都是客户端,也同时都是服务端。只要有一个 socket,多台机器就可以任意通信,不存在哪两台机器是属于一个连接的概念。因此,每一个 UDP 的 socket 都需要 bind。每次通信时,调用 sendto 和 recvfrom,都要传入 IP 地址和端口。

总结

image.png