服务端准备连接的过程

1. 创建 socket

  1. int socket(int domain, int type, int protocol)
  • domain: 用什么样的 socket
    • PF_INET
    • PF_INET6
    • PF_LOCAL
  • type
    • SOCK_STREAM: 表示的是字节流,对应 TCP;
    • SOCK_DGRAM: 表示的是数据报,对应 UDP;
    • SOCK_RAW: 表示的是原始套接字。
  • protocol
    • 原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成。
    • protocol 目前一般写成 0 即可。

2. bind: 设定电话号码

创建出来的套接字如果需要被别人使用,就需要调用 bind 函数把套接字和套接字地址绑定,就像去电信局登记我们的电话号码一样。

bind(int fd, sockaddr * addr, socklen_t len)
  • sockaddr: 通用地址类型, 实现后来的 void * (当时 c 语言中没有 void*)
    • 实际可以传入 IPv4、IPv6 或者本地套接字格式
    • bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析
  • len 字段表示的就是传入的地址长度,它是一个可变值

对于使用者来说,每次需要将 IPv4、IPv6 或者本地套接字格式转化为通用套接字格式:

struct sockaddr_in name;
bind (sock, (struct sockaddr *) &name, sizeof (name)

对于实现者来说,可根据该地址结构的前两个字节判断出是哪种地址。为了处理长度可变的结构,需要读取函数里的第三个参数,也就是 len 字段,这样就可以对地址进行解析和判断了。

通配地址:

  • 相当于告诉操作系统内核:“Hi,我可不挑活,只要目标地址是咱们的都可以。”比如一台机器有两块网卡,IP 地址分别是 202.61.22.55 和 192.168.1.11,那么向这两个 IP 请求的请求包都会被我们编写的应用程序处理。

设置通配地址:

  • 对于 IPv4 的地址来说,使用 INADDR_ANY 来完成通配地址的设置;
  • 对于 IPv6 的地址来说,使用 IN6ADDR_ANY 来完成通配地址的设置。
struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4通配地址 */

端口:

  • 如果把端口设置成 0,就相当于把端口的选择权交给操作系统内核来处理,操作系统内核会根据一定的算法选择一个空闲的端口,完成套接字的绑定。这在服务器端不常使用。

初始化 IPv4 TCP socket 的例子:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

int make_socket (uint16_t port)
{
  int sock;
  struct sockaddr_in name;


  /* 创建字节流类型的IPV4 socket. */
  sock = socket (PF_INET, SOCK_STREAM, 0);
  if (sock < 0)
    {
      perror ("socket");
      exit (EXIT_FAILURE);
    }


  /* 绑定到port和ip. */
  name.sin_family = AF_INET; /* IPV4 */
  name.sin_port = htons (port);  /* 指定端口 */
  name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */
  /* 把IPV4地址转换成通用地址格式,同时传递长度 */
  if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0)
    {
      perror ("bind");
      exit (EXIT_FAILURE);
    }


  return sock;
}

3. listen:接上电话线,一切准备就绪

初始化创建的套接字,可以认为是一个”主动”套接字,其目的是之后主动发起请求(通过调用 connect 函数,后面会讲到)。通过 listen 函数,可以将原来的”主动”套接字转换为”被动”套接字,告诉操作系统内核:“我这个套接字是用来等待用户请求的。”当然,操作系统内核会为此做好接收用户请求的一切准备,比如完成连接队列。

int listen (int socketfd, int backlog)
  • socketdf 为套接字描述符
  • backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小,这个参数的大小决定了可以接收的并发数目。

4. accept: 电话铃响起了……

当客户端的连接请求到达时,服务器端应答成功,连接建立,这个时候操作系统内核需要把这个事件通知到应用程序,并让应用程序感知到这个连接。这个过程,就好比电信运营商完成了一次电话连接的建立, 应答方的电话铃声响起,通知有人拨打了号码,这个时候就需要拿起电话筒开始应答。

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
  • listensockfd 是套接字
  • 结果参数
    • cliadd 是通过指针方式获取的客户端的地址
    • addrlen 告诉我们地址的大小
  • 返回值: 一个全新的描述字,代表了与客户端的连接

**

客户端发起连接的过程

1. 创建 socket

与服务端相同

2. connect: 拨打电话

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
  • 函数的第一个参数 sockfd 是连接套接字,通过前面讲述的 socket 函数创建。
  • 第二个、第三个参数 servaddr 和 addrlen 分别代表指向套接字地址结构的指针和该结构的大小。
  • 套接字地址结构必须含有服务器的 IP 地址和端口号。

如果是 TCP 套接字,那么调用 connect 函数将激发 TCP 的三次握手过程,而且仅在连接建立成功或出错时才返回。其中出错返回可能有以下几种情况:

  • 三次握手无法建立
  • 客户端收到 RST
    • 目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器(如前所述);
    • TCP 想取消一个已有连接;
    • TCP 接收到一个根本不存在的连接上的分节。
  • 客户发出的 SYN 包在网络上引起了”destination unreachable”,即目的不可达的错误

image.png

TCP 三次握手的解读

  1. 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端进入 SYNC_SENT 状态;
  2. 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;
  3. 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 k+1;
  4. 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

总结

  • 服务器端通过创建 socket,bind,listen 完成初始化,通过 accept 完成连接的建立。
  • 客户端通过创建 socket,connect 发起连接建立请求。

精选留言

疾风知劲草

之前看过一些文章解释,为什么tcp建立连接需要三次握手,解释如下

tcp连接的双方要确保各自的收发消息的能力都是正常的。
客户端第一次发送握手消息到服务端,
服务端接收到握手消息后把ack和自己的syn一同发送给客户端,这是第二次握手,
当客户端接收到服务端发送来的第二次握手消息后,客户端可以确认“服务端的收发能力OK,客户端的收发能力OK”,但是服务端只能确认“客户端的发送OK,服务端的接收OK”,
所以还需要第三次握手,客户端收到服务端的第二次握手消息后,发起第三次握手消息,服务端收到客户端发送的第三次握手消息后,就能够确定“服务端的发送OK,客户端的接收OK”,
至此,客户端和服务端都能够确认自己和对方的收发能力OK,,tcp连接建立完成。

作者回复: 赞

鱼向北游

关于三次握手
几句话解释清楚
1.信道不安全 保证通信需要一来一回
2.客户端的来回和服务端的来回 共四次 这是最多四次
3.客户端的回和服务端的来合并成一个,就是那个sync k ack j+1
4.这样就是三次握手

作者回复: 很清楚,很简洁。