TCP的三次握手

服务端准备连接的过程

创建套接字

要创建一个可用的套接字,需要使用下面的函数

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原本是用来指定通信协议的,现在基本废弃。因为协议已经通过前面两个参数指定完成,所以写0就行。

bind:设定电话号码

创建出来的套接字如果需要被别人使用,就需要调用bind函数把套接字和套接字地址绑定。

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

bind第二个参数是通用地址sockaddraddr,这里有一个地方值得注意,那就是虽然接收的是通用地址格式,实际上传入的参数可能是ipv4,ipv6或者本地套接字格式。根据len字段判断传入的addr该怎么解析,len字段表示的是传入的地址长度,它是一个可变值。
可以理解为
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)

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

我们可以把地址设置成本地的ip地址,这相当于告诉操作系统内核,仅仅对目标ip是本机ip地址的ip包进行处理,通过统配地址的设置。
对于ipv4的地址来说,使用inaddr_any来完成统配地址的设置
对于ipv6的地址来说,使用in6addr_any来完成统配地址的设置

struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4 通配地址 */

除了地址,还有端口,如果把端口设置为0,就相当于把端口的控制权交给操作系统内核来处理,操作系统内核会根据一定的算法选择一个空闲的端口,完成套接字的绑定。
一个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
}

listen

初始化创建的套接字,可以认为是一个”主动”套接字,其目的是之后主动发起请求(通过调用connect函数),通过listen函数,可以将原来的”主动”套接字转换为”被动”套接字,告诉操作系统内核会i为此做好接受用户请求的一切准备,比如完成连接队列。
listen函数
int listen (int socketfd, int backlog)
第一个参数socketfd为套接字描述符,第二个参数backlog,官方的解释为未完成连接队列的大小,这个参数的大小决定了可以接收的并发数目,这个参数越大,并发数目理论上也会越大,但是参数过大也会占用过多的系统资源,一些系统,比如linux并不运行对这个参数进行改变,对于backlog整个参数的设置有一些最佳实践。

accept

好比有人打电话过来,需要拿起话筒开始应答。

作用就是连接建立之后,操作系统内核和应用程序之间的桥梁

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

这里一定要注意有两个套接字描述字,第一个是监听套接字描述字listensockfd,它是作为输入参数存在的;第二个是返回的已连接套接字描述字。

客户端发起连接的过程

前面讲述的bind,listen以及accept的过程,是典型的服务器端的过程。
第一步还是一样,建立一个套接字。不一样的是

connect:拨打电话

客户端和服务器端的连接建立,是通过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接收到一个根本不存在的连接上的分节。
3. 客户发出的SYN包在网络上引起了”destination unreachable”,即目的不可达的错误,这种情况比较常见的原因是客户端和服务端路由不同。

著名的TCP三次握手

客户端 服务器
socket,connect(阻塞)(主动打开)SYN_SENT SYN j socket,bind,listen(被动打开)accept(阻塞)SYN_RCVD SYN k,ACK j+1
connect返回 ESTABLISED ACK k+1 accept返回 read阻塞 ESTABLISED

阻塞式,就是调用发起后不会直接返回,又操作系统内核处理之后才会返回,相对的,还有一种叫做非阻塞式的。

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阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入ESTRABLISHED状态。

All posts

Other pages

发表回复

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