24-TCP三次握手:怎么使用套接字格式建立连接?

24-TCP三次握手:怎么使用套接字格式建立连接?1、TCP三次握手:怎么使用套接字格式建立连接?上一讲介绍了IPv4、IPv6以及本地套接字格式;这一讲我们来讲一讲怎么使用这些套接字格式完成连接的建立,当然,经典的TCP三次握手理论也会贯穿其中

1、TCP三次握手:怎么使用套接字格式建立连接?

上一讲介绍了 IPv4、IPv6 以及本地套接字格式;

这一讲我们来讲一讲怎么使用这些套接字格式完成连接的建立,当然,经典的 TCP 三次握手理论也会贯穿其中。

希望经过这一讲的讲解,你会牢牢记住 TCP 三次握手和客户端、服务器模型。

1.1、服务端准备连接的过程

1.1.1、创建套接字

创建套接字函数:

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

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

调用 bind 函数的方式如下:

bind(int fd, sockaddr * addr, socklen_t len)

bind 函数后面的第二个参数是通用地址格式sockaddr * addr

注意: 虽然接收的是通用地址格式,实际上传入的参数可能是 IPv4、IPv6 或者本地套接字格式。

bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析,len 字段表示的就是传入的地址长度,它是一个可变值。

其实可以把 bind 函数理解成这样:

bind(int fd, void * addr, socklen_t len)

不过 BSD 设计套接字的时候大约是 1982 年,那个时候的 C 语言还没有void *的支持,为了解决这个问题,BSD 的设计者们创造性地设计了通用地址格式来作为支持 bind 和 accept 等这些函数的参数。

对于使用者来说,每次需要将 IPv4、IPv6 或者本地套接字格式转化为通用套接字格式,就像下面的 IPv4 套接字地址格式的例子一样:

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

设置 bind 的时候,对地址和端口可以有多种处理方式:

1、地址处理

可以把地址设置成本机的 IP 地址,这相当告诉操作系统内核,仅仅对目标 IP 是本机 IP 地址的 IP 包进行处理;

问题: 编写应用程序时并不清楚自己的应用程序将会被部署到哪台机器上

解决方法: 通配地址

通配地址: 通配地址相当于告诉操作系统内核:“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通配地址 */

2、端口处理

  1. 端口设置成 0,就相当于把端口的选择权交给操作系统内核来处理,操作系统内核会根据一定的算法选择一个空闲的端口,完成套接字的绑定。这在服务器端不常使用
  2. 一般来说,服务器端的程序一定要绑定到一个众所周知的端口上
1.1.3、初始化 IPv4 TCP 套接字的例子
#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
}
1.1.4、listen:接上电话线,一切准备就绪

bind 函数只是让我们的套接字和地址关联;

listen: 让服务器真正处于可接听的状态

初始化创建的套接字,可以认为是一个”主动”套接字,其目的是之后主动发起请求(通过调用 connect 函数,后面会讲到)。

通过 listen 函数,可以将原来的”主动”套接字转换为”被动”套接字,告诉操作系统内核:“我这个套接字是用来等待用户请求的。

当然,操作系统内核会为此做好接收用户请求的一切准备,比如完成连接队列。

listen 函数:

int listen (int socketfd, int backlog)

第一个参数 socketdf 为套接字描述符;

第二个参数 backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小,这个参数的大小决定了可以接收的并发数目。这个参数越大,并发数目理论上也会越大。但是参数过大也会占用过多的系统资源,一些系统,比如 Linux 并不允许对这个参数进行改变。

1.1.5、accept: 电话铃响起了……

当客户端的连接请求到达时,服务器端应答成功,连接建立,这个时候操作系统内核需要把这个事件通知到应用程序,并让应用程序感知到这个连接。

连接建立之后,你可以把 accept 这个函数看成是操作系统内核和应用程序之间的桥梁。它的原型是:

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)

第一个参数 listensockfd 是套接字,可以叫它为 listen 套接字,因为这就是前面通过 bind,listen 一系列操作而得到的套接字。

函数的返回值:

有两个部分,

第一个部分 cliadd通过指针方式获取的客户端的地址addrlen 告诉我们地址的大小,这可以理解成当我们拿起电话机时,看到了来电显示,知道了对方的号码;

另一个部分是函数的返回值,这个返回值是一个全新的描述字,代表了与客户端的连接。

一定要注意有两个套接字描述字:

  • 第一个是监听套接字描述字 listensockfd,它是作为输入参数存在的;
  • 第二个是返回的已连接套接字描述字。

为什么要把两个套接字分开呢?

  • 监听套接字一直都存在,它是要为成千上万的客户来服务的,直到这个监听套接字关闭;
  • 一旦一个客户和服务器连接成功,完成了 TCP 三次握手,操作系统内核就为这个客户生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信处理;
  • 如果应用服务器完成了对这个客户的服务,比如一次网购下单,一次付款成功,那么关闭的就是已连接套接字,这样就完成了 TCP 连接的释放;
  • 最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务。

1.2、客户端发起连接的过程

下面介绍客户端发起连接请求的过程;

第一步还是和服务端一样,要建立一个套接字;

不一样的是客户端需要调用 connect 向服务端发起请求。

1.2.1、connect: 拨打电话
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)

第一个参数 sockfd: 是连接套接字,通过前面讲述的 socket 函数创建;

第二个、第三个参数 servaddr 和 addrlen :分别代表指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有服务器的 IP 地址和端口号。

客户在调用函数 connect 前不必非得调用 bind 函数,因为如果需要的话,内核会确定源 IP 地址,并按照一定的算法选择一个临时端口作为源端口。

如果是 TCP 套接字,那么调用 connect 函数将激发 TCP 的三次握手过程,而且仅在连接建立成功或出错时才返回。

其中出错返回可能有以下几种情况:

  1. 三次握手无法建立,客户端发出的 SYN 包没有任何响应,于是返回 TIMEOUT 错误。这种情况比较常见的原因是对应的服务端 IP 写错
  2. 客户端收到了 RST(复位)回答,这时候客户端会立即返回 CONNECTION REFUSED 错误。

​ 这种情况比较常见于客户端发送连接请求时的请求端口写错,因为 RST 是 TCP 在发生错误时发送的一种 TCP 分节。

产生 RST 的三个条件是:

  • 目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器(如前所述);
  • TCP 想取消一个已有连接;
  • TCP 接收到一个根本不存在的连接上的分节。
  1. 客户发出的 SYN 包在网络上引起了”destination unreachable”,即目的不可达的错误。原因是客户端和服务器端路由不通。

1.3、著名的 TCP 三次握手: 这一次不用背记

img

我们使用的网络编程模型都是阻塞式的。

阻塞式:就是调用发起后不会直接返回,由操作系统内核处理之后才会返回。

相对的,还有一种叫做非阻塞式的,我们在后面的章节里会讲到。

1.3.1、TCP 三次握手的解读
  • 服务器端通过 socket,bind 和 listen 完成了被动套接字的准备工作,被动的意思就是等着别人来连接,然后调用 accept,就会阻塞在这里,等待客户端的连接来临;
  • 客户端通过调用 socket 和 connect 函数之后,也会阻塞。接下来的事情是由操作系统内核完成的,更具体一点的说,是操作系统内核网络协议栈在工作。

具体的过程:

  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 状态。

应答过程总共进行了三次,这就是 TCP 连接建立之所以被叫为“三次握手”的原因了。

总结

分别从服务端和客户端的角度,讲述了如何创建套接字,并利用套接字完成 TCP 连接的建立。

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

思考题

1、既然有阻塞调用,就应该有非阻塞调用,那么如何使用非阻塞调用套接字呢?使用的场景又是哪里呢?

非阻塞调用的场景就是高性能服务器编程!

我所有的调用都不需要等待对方准备好了再返回,而是立即返回,那么我怎么知道是否准备好了?

就是把这些fd注册到类似select或者epoll这样的调用中,变多个fd阻塞为一个fd阻塞,只要有任何一个fd准备好了,select或者epoll都会返回,然后我们在从中取出准备好了的fd进行各种IO操作。

2、客户端发起 connect 调用之前,可以调用 bind 函数么?

​ 可以调用bind函数,但是这样一来客户端会使用固定端口,容易产生冲突,另外,一般而言我们希望客户端具备快速启动的能力,但是因为TIMEWAIT状态的存在,使用随机端口会更容易处理一些。

3、在 TCP 建立连接的三次握手连接阶段,如果客户端发送的第三个ACK包丢了,那么客户端和服务端分别进行什么处理呢?

Server 端

  • 第三次的ACK在网络中丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。

  • 而Server重发SYN+ACK包的次数,可以通过设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.

  • 如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。

Client 端

  • 在linux c 中,client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。

  • 那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。

今天的文章24-TCP三次握手:怎么使用套接字格式建立连接?分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/60328.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注