TCP/IP协议及网络编程基础

时间:2021-08-04 02:42:22

本地的进程间通信(IPC)有很多种方式,可以总结为下面4类:

  • 消息传递(管道、FIFO、消息队列);
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量);
  • 共享内存(匿名的和具名的);
  • 远程过程调用(Solaris门和Sun RPC)。

1、TCP/IP

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

2、什么是socket

TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口一样,TCP/IP也必须对外提供编程接口,即Socket编程接口。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,本质上是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。同时socket接口也支持出TCP/IP之外的其他协议的接口编程。

由于socket提供了TCP/IP协议族的接口,因此,使用TCP/IP协议的应用程序通常采用UNIX BSD的套接字(socket)接口来实现网络进程之间的通信——即:网络中的进程通过socket进行通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我们所说的“一切皆socket”。

既然已经知道网络中的进程是通过socket来通信的,那什么是socket呢?

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket本质就是一种特殊的文件,有一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

socket解释:一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。

2.1、表示网络中的进程

既然网络中的进程通过socket进行通信。那首先要解决的问题是如何唯一标识一个进程,否则通信无从谈起!

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络中计算机的进程了,网络中的进程通信就可以利用这个标志完成进程间交互。

2.2、通过socket完成通信

一个生活中的场景。你要打电话给一个朋友,先拨号并等待朋友接听,同时朋友听到电话铃声后并在自己空闲时接听电话,这时你和你的朋友就建立起了连接,就可以互相讲话了。等交流结束,挂断电话结束此次交谈。

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞并等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

2.3、TCP、UDP(面向连接和无连接)

TCP 和 UDP 都是构建在 IP 之上的。因此,IP 是构建整个 TCP/IP 协议族的基础。但 IP 提供的是一种尽力而为的、不可靠的无连接服务。它接收来自其上层的分组,将它们封装在一个 IP 分组中,根据路由为分组选择正确的硬件接口,从这个接口将分组发送出去。一旦将分组发送出去了,IP 就不再关心这个分组了。和所有无连接协议一样,它将分组发送出去之后就不再记得这个分组了。

这种简单性也是 IP 的主要优点。因为它对底层的物理介质没有作任何假设,所以在任何能够承载分组的物理链路上都可以运行 IP。例如,IP 可以运行在简单的串行链路、以太网和令牌环 LAN、X.25 和使用 ATM(Asychronous Transfer Mode,异步转移模式)的 WAN、CDPD(Cellular Digital Packet Data,无线蜂窝数字分组数据)网,以及很多其他网络上。尽管这些网络技术之间有很大的差异,但 IP 对它们一视同仁,除了认为它们可以转发分组之外没有对其作任何假设。这种机制隐含了很深的意义。IP 可以运行在任何能够承载分组的网络上,所以整个TCP/IP 协议族也可以。

TCP 在 IP 之上的扩展

现在我们来看看 TCP 是怎样利用这种简单的无连接服务来提供可靠的面向连接服务的。TCP 的分组被称为段(segment),是放在 IP 数据报中发送的,因此,根本无法假定这些分组会抵达目的地,更不用说保证分组无损坏且以原来的顺序到达了。

为了提供这种可靠性,TCP 向基本的 IP 服务中添加了三项功能:

  1. 首先,它为 TCP 段中的数据(TCP报文的数据部分)提供了校验和。这样有助于确保抵达目的地的数据在传输过程中不会被网络损坏。
  2. 第二,它为每字节分配了一个序列号,这样,如果数据抵达目的地时真的错序了,接收端也能够按照恰当的顺序将其重装起来。当然,TCP 并没有为每字节都附加一个序列号。实际上,每个 TCP 段的首部都包含了段中第一字节的序列号。这样,就隐含地知道了段中其他字节的序列号。
  3. 第三,TCP 提供了一种确认-重传机制,以确保最终每个段都会被传送出去。

UDP 在 IP 之上的扩展

另一方面,UDP 为编写应用程序的程序员提供了一种不可靠的无连接服务。事实上,UDP 只向底层的 IP 协议中添加了两项功能。

  1. 首先,它提供了一个可选的校验和来检测数据的损坏情况。尽管 IP 也有校验和,但它只对 IP 分组首部进行计算,所以,TCP 和 UDP 也都提供了校验和来保护它们自己的首部和数据。
  2. 其次,UDP 向 IP添加的第二项特性就是端口的概念。

TCP 和 UDP 的类比

对于TCP和UCP,一种标准的类比是:使用无连接协议就像寄信,而使用面向连接的协议就像打电话。

给朋友寄信时,每封信都是一个独立寻址且自包含的实体。邮局在处理这些信件时不会考虑到两个通信者之间的任何其他信件。邮局不会维护以往通信者的历史记录–也就是说,它不会维护信件之间的状态。邮局也不保证信件不丢失、不延迟、不错序。这种方式就对应于无连接协议发送数据报的方式。(用明信片进行类比会更合适一些,因为写错地址的信件会被退回发信人,而(和典型的无连接协议数据报一样)明信片则不会。)

现在来看看不是给朋友寄信,而是打电话时会发生些什么事情。

  1. 首先,拨朋友的号码来发起呼叫并等待。朋友应答,会说“嗨”之类的话,然后我们回应:“嗨,Lucy。我是 Mike。”,接着我们和朋友聊一会儿,然后互说再见并挂机。这是面向连接协议中发生的典型状况。在连接建立阶段,一端与其对等实体联系(connect),交换初始问候信息,对会话中要用到的一些参数和选项进行沟通(handshake),然后连接进入数据传输阶段
  2. 在电话交谈的过程中,两端用户都知道他们在和谁说话,因此没必要不停地说“这是 Mike 在跟 Lucy 说话”。也没必要在每次说话之前都拨一次朋友的电话号码——我们的电话已经连接起来了。同理,在面向连接协议的数据传输阶段,也没必要说明我们自己或对等实体的地址。连接为我们维护的状态中包含了这些地址。我们只要发送数据就行了,不需要考虑寻址或其他与协议相关的问题
  3. 就像用电话交谈一样,连接的任一端完成数据的传输时,都要通知其对等实体。两端都完成传输时,要依次将连接拆除。

总结
可以把 TCP 连接中的网络地址当作一个办公室总机的电话号码,把端口号当作办公室中某台正被呼叫的特定电话的分机号。同理,可以将UDP网络地址当作一座公寓楼的地址,并把端口号当作公寓楼大厅中的个人邮箱。

3、socket的基本操作

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

3.1、socket()函数

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

socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述符,而这个socket描述符跟文件描述符一样,后续的操作都会用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
  • protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则调用connect()、listen()时系统会自动随机分配一个端口。

3.2、bind()函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

函数的三个参数分别为:

  • sockfd:即socket描述字,它是通过socket()函数创建了的socket的唯一标识。bind()函数就是将要给这个描述字绑定一个协议地址。
  • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。
  • addrlen:对应的是地址的长度。

注:第二个参数的协议地址结构为专用地址——根据创建socket时的地址协议族的不同而不同,例如:

ipv4对应的是:

 struct sockaddr_in {
    sa_family_t      sin_family; 
    in_port_t        sin_port;   
    struct in_addr   sin_addr;   
};

struct in_addr {
    uint32_t         s_addr;     
};

ipv6对应的是:

 struct sockaddr_in6 { 
    sa_family_t       sin6_family;    
    in_port_t         sin6_port;      
    uint32_t          sin6_flowinfo;  
    struct in6_addr   sin6_addr;      
    uint32_t          sin6_scope_id;  
};

struct in6_addr { 
    unsigned char     s6_addr[16];    
};

Unix域对应的是:

#define UNIX_PATH_MAX 108
struct sockaddr_un { 
    sa_family_t   sun_family;                
    char          sun_path[UNIX_PATH_MAX];   
};

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,操作系统会自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  • Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。

3.3、listen()、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函数的第一个参数即为要监听的服务器端的socket描述字,第二个参数为相应socket监听队列中允许保持的尚未处理的可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,服务器端通过调用listen函数将socket变为被动类型,等待客户的连接请求。(一般listen会立即返回,成为监听socket)

connect函数的第一个参数即为客户端的socket描述字,第二参数为要连接的服务器的socket地址,第三个参数为socket地址的长度。(函数参数中出现socket地址的地方,基本上还加一个地址长度参数) 。客户端通过调用connect函数来建立与TCP服务器的连接。

3.4、accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数的第一个参数为服务器端的socket描述字,第二个参数为指向 struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accept成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。(第二第三个参数用来返回连接的客户端socket地址)

注意:

  1. accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为“监听socket”;而accept函数返回的是“已连接socket”。一个服务器通常仅仅只创建一个“监听socket”,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受了的客户连接创建一个“已连接socket”,当服务器完成了对某个客户的服务,相应的“已连接socket”就应该被关闭。而“监听socket”一般在服务器最后才关闭。
  2. server调用accept会阻塞等待client的连接请求。因此三次握手时,会发生server的accept阻塞和client的connect阻塞。

3.5、read()、write()等函数

至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!

网络I/O操作有下面几组:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数。

  • read函数负责从fd中读取内容至缓冲区。读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
  • write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们用write向套接字文件描述符写时有俩种可能。
    1. write的返回值大于0,表示写了部分或者是全部的数据;
    2. 返回的值小于0,此时出现了错误。

我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

3.6、close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

默认情况下,close()会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据。也就意味着,调用 close()将丢失内核输出缓冲区中的数据。

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

4、socket的建立和关闭

4.1、socket中TCP的三次握手建立连接详解

客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证 IP 地址、端口、物理链路等正确无误,为数据的传输开辟通道。

我们知道 TCP 建立连接要进行“三次握手”,即交换三个分组。可以形象的比喻为下面的对话:

  • [Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。(SYN)”
  • [Shake 2] 套接字B:“好的,我这边已准备就绪。(SYN+ACK)”
  • [Shake 3] 套接字A:“谢谢你受理我的请求。(ACK)”

TCP/IP协议及网络编程基础

三次握手的流程及状态转移、socket API

  1. 第一次握手:Client调用connect()触发连接请求,将标志位SYN置为1,随机产生一个ISN:seq=J,并将该报文段(即同步报文段)发送给Server,至此Client进入SYN_SENT状态,等待Server确认(此时connect阻塞)。
  2. 第二次握手:Server依次调用socket()、bind()、listen()之后,就会监听指定的socket地址。Server收到客户端的同步报文段之后由标志位SYN=1监听到Client连接请求,因此Server调用accept()函数接受连接请求,并将标志位SYN和ACK都置为1,确认号ack=J+1,随机产生一个序号seq=K,并将该报文段(同步+确认报文段)发送给Client以确认连接请求,至此Server进入SYN_RCVD状态。
  3. 第三次握手:Client收到server的报文段后,检查ACK是否为1,ack是否为J+1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Client进入ESTABLISHED状态,Server收到client报文段之后检查ACK是否为1,ack是否为K+1,如果正确则连接建立成功,Server也进入ESTABLISHED状态,完成三次握手。随后Client与Server之间可以开始传输数据了。

TCP/IP协议及网络编程基础

总结:
  • 三次握手(Three-Way Handshake)指建立一个TCP连接时,需要客户端和服务端总共 发送3个包 以确认连接的建立。每一次发送报文段即是一次握手。在socket编程中,这一过程由 客户端执行connect 来触发。
  • 客户端的connect在三次握手的第二次返回(第三次发送确认报文段之前,和client的ESTABLISHED同时),而服务器端的accept在三次握手的第三次返回(和server的ESTABLISHED同时)。

4.2、TCP的数据传输

建立连接后,两台主机就可以相互传输数据了。

4.2.1、数据不丢失传输200字节

TCP/IP协议及网络编程基础

首先,主机A通过1个数据包发送100个字节的数据,数据包的 Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。

为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

此时 Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认 Ack 号:

Ack号 = Seq号 + 传递的字节数 + 1

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。

4.2.2、数据包/确认包有丢失传输200字节

TCP/IP协议及网络编程基础

上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。

为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。

上图演示的是数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。

重传超时时间(RTO, Retransmission Time Out)

这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。

往返时间(RTT,Round-Trip Time)表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延。

重传次数

TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。

最后需要说明的是,发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据。

4.3、socket中TCP的四次挥手释放连接详解

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

断开连接的四次挥手,可以形象的比喻为下面的对话:

  • [Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
  • [Shake 2] 套接字B:“好的,我知道了。我再处理一下,稍等一会。”
  • 等待片刻后……
  • [Shake 3] 套接字B:“好了,可以断开连接了。”
  • [Shake 4] 套接字A:“好的,谢谢合作。”

TCP/IP协议及网络编程基础

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了(read/recv会返回0),但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。

四次挥手的流程和状态转移

  • 第一次挥手:Client调用close执行主动关闭连接,将标志位FIN置1(结束报文段),用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
  • 第二次挥手:Server收到结束报文段后,检测到设置了FIN标志位,执行被动关闭,确认序号为收到序号+1,并发送一个ACK给Client,Server进入CLOSE_WAIT状态(此时read返回0)。client收到确认报文段后,进入FIN_WAIT_2状态。
  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
  • 第四次挥手:Client收到FIN后,接着发送一个ACK给Server,Client进入TIME_WAIT状态,server确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

TCP/IP协议及网络编程基础

总结
  • 四次挥手(Four-Way Wavehand)指断开一个TCP连接时,需要客户端和服务端总共 发送4个包 以确认连接的断开。在socket编程中,这一过程由 客户端或服务端任一方执行close 来触发。

4.4、其他问题

4.4.1、为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,client把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。

4.4.2、关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。

客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为

报文最大生存时间(MSL,Maximum Segment Lifetime)

。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

5、域名

12 个阿拉伯数字很难记忆。使用一个名称更容易。

用于 TCP/IP 地址的名字被称为域名。w3school.com.cn 就是一个域名。

当你键入一个像 http://www.w3school.com.cn 这样的域名,域名会被一种 DNS 程序翻译为数字。

在全世界,数量庞大的 DNS 服务器被连入因特网。DNS 服务器负责将域名翻译为 TCP/IP 地址,同时负责使用新的域名信息更新彼此的系统。

当一个新的域名连同其 TCP/IP 地址一同注册后,全世界的 DNS 服务器都会对此信息进行更新。