Linux:socket通信原理

linux socket通信

一、网络中进程之间通信

  1. 操作系统为进程间通信提供的常见方式:
    UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal);
    UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
  2. 为了唯一标识某个主机进程,可以通过TCP/IP协议族,即网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。三元组(ip地址,协议,端口)就可以标识网络间进程进行通信——TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)。

二、Socket简介

TCP/IP协议族包括运输层、网络层、链路层,而socket位置如下,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
在这里插入图片描述
应用层 (Application):应用层为操作系统或网络应用程序提供访问网络服务的接口, 有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应用。eg.BGP[采用TCP 179端口]、FTP、TFTP、HTTP、SMTP、DHCP、Telnet、DNS和SNMP、BFD[采用udp封装,目的端口号3784,源端口号在49152到65535的范围内]等。

传输层 (Transport):定义了一些传输数据的协议和端口号识别应用层协议,TCP的数据单元称为段 (segments)而UDP协议的数据单元称为“数据报(datagrams)。这个层负责获取全部信息,因此,它必须跟踪数据单元碎片、乱序到达的数据包和其它在传输过程中可能发生的危险。第4层为上层提供端到端(最终用户到最终用户)的透明的、可靠的数据传输服务。其中:TCP面向连接端到端的可靠数据传输----提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。UDP面向无连接不可靠的传输,无流控或差错恢复功能。一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。

网络层 (Network):在计算机网络通信间选择合适的网间路由和交换结点,确保数据及时传送。网络层将数据链路层提供的帧组成数据包,封装有网络层包头——逻辑地址IP信息。主要处理路由,地址解析【ARP】以及实现拥塞控制、网际互连等功能。数据单位称为数据包(packet)。网络层协议的代表包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。IP头协议号说明IP报文中承载的协议:传输层协议TCP是6 UDP是17,或者网络层协议。具体协议号可以参考 https://blog.csdn.net/shiguagnmanbu/article/details/77621546

数据链路层(Link):通过差错控制提供数据帧(Frame)在信道上无差错的传输。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等,通过帧头的类型字段识别。
【ARP地址解析】首先通过IP协议来判断两台主机是否在同一个子网中,如果在同一个子网,就通过ARP协议查询对应的MAC地址,然后以广播的形式向该子网内的主机发送数据包;如果不在同一个子网,以太网会将该数据包转发给本子网的网关进行路由。网关是互联网上子网与子网之间的桥梁,所以网关会进行多次转发,最终将该数据包转发到目标IP所在的子网中,然后再通过ARP获取目标机MAC,最终也是通过广播形式将数据包发送给接收方。而完成这个路由协议的物理设备就是路由器,路由器扮演着交通枢纽的角色,它会根据信道情况,选择并设定路由,以最佳路径来转发数据包。

物理层(PhysicalLayer):主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是用以建立、维护和拆除物理链路连接,传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后在转化为1、0,也就是我们常说的数模转换与模数转换),这一层的数据叫做比特。具体地讲,机械特性规定了网络连接时所需接插件的规格尺寸、引脚数量和排列情况等;电气特性规定了在物理连接上传输bit流时线路上信号电平的大小、阻抗匹配、传输速率 距离限制等;功能特性是指对各个信号先分配确切的信号含义,即定义了DTE和DCE之间各个线路的功能;规程特性定义了利用信号线进行bit流传输的一组 操作规程,是指在物理连接的建立、维护、交换信息是,DTE和DCE双放在各电路上的动作系列。典型规范代表包括:EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45等。

Socket: Socket是应用层与TCP/IP协议族通信的中间软件抽象层。而TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议的网络服务通过OS提供增加支持TCP/IP的系统调用——Berkeley套接字,如Socket,Connect,Send,Recv等。Socket起源于Unix遵循“一切皆文件”,用“打开open –> 读写write/read –> 关闭close”模式来操作。

1、引入
正如在网络中,由IP地址可以唯一确定一台主机,但是准确来说,网络通讯中的双方并不是主机,而是运行在主机上的进程,这样就需要进一步确定是主机中的哪个进程要进行网络通讯。因此,除了IP地址之外,还需要端口号来唯一确定主机中的通讯进程。IP地址和端口号就构成了一个网络中的唯一标识符,即套接字——对通信端点的抽象。
套接字允许两个进程进行通讯,这两个进程可能运行在同一台机器上,也可能运行在不同的机器上。更准确地说,套接字是使用标准Unix文件描述符来与其它计算机进行通讯的一种方式。
在Unix操作系统中,每一个读写操作都是通过读写文件描述符来完成的。一个文件描述符就是一个与打开的文件相关联的整数,它可以是一个网络连接、一个文本文件、一个终端或其它东西。
作为特殊的文件,一些socket函数通过一些读写IO/Open/Close等函数对其进行操作,Socket是一组接口将复杂的TCP/IP协议族隐藏后,方便用户操作该接口,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。

2、用途
Socket被用于客户端/服务端应用框架中。服务端是一个针对客户端的请求执行某些特定操作的进程。大多数应用层协议如FTP、SMTP和POP3使用Socket来建立客户端与服务端之间的连接,从而进行数据的交换。

3、类型
一般情况下假定进程间使用同种类型的socket进行通讯,但事实上不同类型的socket之间并没有通讯上的限制。常见的套接字类型有三种。前两种被广泛地使用,而后一种使用较少。
1)流套接字:提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。如果你通过流套接字发送三个字符”A, B, C”,它们将会以同样的顺序到达——”A, B, C”,原因在于流套接字使用TCP(传输控制协议)进行数据传输。如果传输失败,发送方将会收到错误提示符。
2)数据包套接字:提供无连接服务。你无需像使用流套接字那样建立一个连接,而只需将目的地址信息一同打包后发送出去。该服务使用UDP(用户数据报协议)进行传输,延迟小且效率高,缺点是不能保证数据传输的可靠性。
3)原始套接字:允许用户对底层通讯协议进行访问。能够对底层的传输机制进行控制,因此可以用原始套接字来操纵网络层和传输层应用。原始套接字并不是给普通用户使用的,它们主要被用于开发新的通讯协议,或是用来获取已有通讯协议的一些隐蔽功能的访问权限。

4、套接字描述符(一个整数)
如程序通过文件描述符访问文件一样,每个活动的套接字都有一个整数来标识,我们将其称为套接字描述符——套接字描述符是访问套接字的一种路径。操作系统为每个进程维护一个单独的套接字描述符表【每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲中。】
类似于我们熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。Socket的API是操作系统的一部分,集成在I/O中。而返回值就是描述符(descriptor)来标识这个套接字,基于该描述符引用该套接字,进行网络数据send/rev操作。
调用socket与调用open相类似。在两种情况下,均可获得用于输入/输出的文件描述符。当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。
虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。下表总结了到目前为止所讨论的大多数使用文件描述符的函数处理套接字描述符时的行为。未规定的和由实现定义的行为通常意味着函数不能处理套接字描述符。例如,lseek不处理套接字,因为套接字不支持文件偏移量的概念。
表:使用文件描述符的函数处理套接字时的行为
5、涉及函数
1)int socket(int domain, int type, int protocol);
参数说明:  
a)domain:
协议域,又称协议族(family)确定通信的特性,包括地址格式。AF 表示ADDRESS FAMILY 地址族.常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。linux/socket.h中定义:
AF_UNIX/AF_LOCAL:创建只在本机内进行通信的套接字.
AF_INET: 使用IPv4 TCP/IP协议.
AF_INET6:使用IPv6 TCP/IP协议.
AF_UNSPEC域可以代表任何域。历史上,有些平台支持其他网络协议(如AF_IPX为NetWare协议族),但这些协议的域常数没有在POSIX.1标准中定义。
b)type:
确定套接字的类型,进一步确定通信特征。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。如下:
SOCK_RAW:创建原始套接字.
对于数据报(SOCK_DGRAM)接口,创建UDP数据报套接字.与对方通信时是不需要逻辑连接的。只需要送出一个报文,其地址是一个对方进程所使用的套接字。数据报是一种自包含报文。发送数据报近似于给某人邮寄信件。可以邮寄很多信,但不能保证投递的次序,并且可能有些信件丢失在路上。每封信件包含接收者的地址,使这封信件独立于所有其他信件。每封信件可能送达不同的接收者。

SOCK_STREAM:创建TCP流套接字。因此数据报提供了一个无连接的服务。另一方面,字节流(SOCK_STREAM)要求在交换数据之前,在本地套接字和与之通信的远程套接字之间建立一个逻辑连接。使用面向连接的协议通常就像与对方打电话。首先,需要通过电话建立一个连接,连接建立好之后,彼此能双向地通信。每个连接是端到端的通信信道。会话中不包含地址信息,就像呼叫的两端存在一个点对点的虚拟连接,并且连接本身暗含特定的源和目的地。

对于SOCK_STREAM套接字,应用程序意识不到报文界限,因为套接字提供的是字节流服务。这意味着当从套接字读出数据时,它也许不会返回所有由发送者进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用得到。

SOCK_SEQPACKET套接字和SOCK_STREAM套接字很类似,但从该套接字得到的是基于报文的服务而不是字节流服务。这意味着从SOCK_SEQPACKET套接字接收的数据量与对方发送的一致。流控制传输协议(Stream Control Transimission Portocol, SCTP)提供了因特网域上的顺序数据包服务。

SOCK_RAW套接字提供一个数据报接口用于直接访问下面的网络层(在因特网域中为IP)。使用这个接口时,应用程序负责构造自己的协议首部,这是因为传输协议(TCP和UDP等)被绕过了。当创建一个原始套接字时需要有超级用户特权,用以防止恶意程序绕过内建安全机制来创建报文。
c)protocol:
指定协议,通常是0,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。参数protocol通常设置为0,表示通过参数domain指定的协议族和参数type指定的套接字类型确定使用的协议。在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP(传输控制协议)。在AF_INET通信域中套接字类型SOCK_DGRAM的默认协议是UDP(用户数据报协议)。
在这里插入图片描述
d)返回值:
如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符已经上述介绍了。

2)int shutdown(int socketfd,int how) --#include <sys/socket.h>
套接字通信是双向的。可以采用函数shutdown来禁止套接字上的输入/输出。
返回值:若成功则返回0,出错则返回-1
如果how是SHUT_RD(关闭读端),那么无法从套接字读取数据;如果how是SHUT_WR(关闭写端),那么无法使用套接字发送数据;使用SHUT_RDWR则将同时无法读取和发送数据。
与close区别:首先,close关闭一个指向文件的文件描述符,其实只是关闭了这个文件描述符对文件表的指针,close只有在最后一个活动引用被关闭时才释放网络端点。这意味着如果复制一个套接字(例如采用dup),套接字直到关闭了最后一个引用它的文件描述符之后才会被释放【只有当关闭的文件描述符是最后一个指向文件的文件描述符】。而shutdown允许使一个套接字处于不活动状态,关闭对一文件的读写等属性,不管有多少个文件描述符对该文件引用。其次,有时只关闭套接字双向传输中的一个方向会很方便。例如,如果想让所通信的进程能够确定数据发送何时结束,可以关闭该套接字的写端,然而通过该套接字读端仍可以继续接收数据。

  1. 类比
    了解套接字这个抽象概念的最简单的方法是想象一下操作系统中的数据结构。当应用程序调用socket后,操作系统分配一个新的数据结构来保存通信所需的信息,并在进程套接字描述符表内增加一个条目,存储指向这个数据结构的指针。虽然套接字的内部数据结构包含很多字段,但是系统创建套接字后,大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。
    当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。
    对于每个程序系统都有一张单独的表。精确地讲,系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
    4)文件描述符和文件指针的区别
    文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
    文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
    详细内容请看linux文件系统:http://blog.csdn.net/hguisu/article/details/6122513#t7

6、 其他的SOCKET接口函数

1) 套接字地址结构
结构体sturct sockaddr定义了一种通用的套接字地址,它在linux/socket.h中的定义代码如下:

struct sockaddr
	{
	    unsigned short   sa_family;
	    char             sa_data[14];
	}

其中,成员sa_family表示套接字的协议族类型,对于TCP/IP协议该值为AF_INET;成员sa_data储存具体的协议地址. sa_data之所以被定义成14个字节,因为有的协议族使用较长的地址格式.
一般编程中并不对该结构体进行操作,而是使用另一个与它等价的数据结构:sockadd_in.
每一种协议族都有自己的协议地址格式,TCP/IP协议族的地址格式为结构体struct socketadd_in.他在nettinet/in.h中定义格式如下:

struct sockaddr_in
	{
	    unsigned short      sin_family;   //地址类型
	    unsigned short int  sin_port;     //端口号
	    struct   in_addr    sin_addr;     //IP地址
	    unsigned char       sin_zero[8];  //填充字节
	}

其中,成员sin_family表示地址类型,对于使用TCP/IP的协议进行网络编程,该值只能是AF_INET.sin_port为端口号,sin_addr用来储存32位IP地址,数组sin_zero为填充字段,一般赋值为0.

struct in_addr 的定义如下:
struct in_addr
{
    unsigned long s_addr;
}

结构体sockaddr的长度为16字节,结构体sockaddr_in的长度也为16字节.通常在编写基于TCP/IP协议的网络程序时,使用结构体sockaddr_in来设置地址,然后通过强制类型转换成sockaddr类型。
以下是设置地址信息的示例代码:

struct sockaddr_in sock;
	sock.sin_family = AF_INET;
	sock.sin_port   = htons(80);   //设置端口号80
	sock.sin_addr.s_addr = inet_addr("114.114.114.1");
	memset(sock.sin_zero, 0, sizeof(sock.sin_zero));

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

2)初始化套接字

int socket(int domain, int type, int protocol);//返回sockfd

sockfd是描述符。对应于普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数上述已经介绍过。而当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

//eg.下面代码创建一个TCP套接字:
int sock_fd;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd < 0)
{
    perrno("socket");
    exit(1);    
}

//创建UDP套接字为:
sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
  1. 绑定套接字
    socket函数只是创建了一个套接字,这个套接字将工作在那个端口上,程序并没有指定,在客户机/服务器模型中,服务器端的IP地址和端口号一般是固定的,因此在服务器的程序中,使用bind函数将一个套接字和某个端口绑定在一起,该函数一般只由服务器调用. bind()函数把一个地址族中的特定地址赋给socket,一个套接字和某一个端口绑定在一起。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
    函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr* my_addr, socklen_t address_len);

参数说明:
sockfd:一个套接字描述符。 通过socket()函数创建,唯一标识一个socket,bind()函数就是将给这个描述字绑定一个ip+port。
my_addr:是一个const struct sockaddr *结构指针,该结构中包含了要结合的地址和端口号。 指定了sockfd将绑定到的本地地址,可以将参数my_addr的sin_addr设置为INADDR_ANY,而不是某个确定的IP就可以绑定到任何网络接口, 对于只有一个IP地址的计算机,INADDR_ANY对应的就是它的IP地址;对于多宿主主机(拥有多块网卡), INADDR_ANY表示服务器程序将处理来自所有网络接口上相应端口的连接请求。

//这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};
//ipv6对应的是: 
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};
//Unix域对应的是: 
#define UNIX_PATH_MAX    108
struct sockaddr_un { 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

address_len:确定address缓冲区的长度,即对应的是地址的长度。
返回值: 如果函数执行成功,返回值为0,否则为SOCKET_ERROR。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;
而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
eg. 该函数的常见用法如下:

struct sockadd_in serv_addr;
memset(&serv_addr, 0, sizeof(struct sockaddr_in));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port   = htons(80);
serv_addr.sin_addr.s_addr = hton1(INADDR_ANY);
if(bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr_in)) < 0)
{
    perror("bind");
    exit(1);
}

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

4)建立连接
函数connect用来在一个指定的套接字上创建一个连接。客户端通过调用connect函数来建立与TCP服务器的连接。

#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr * serv_addr, socklen_t addrlen);

connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。
本函数用于创建与指定外部端口的连接。sockfd参数指定一个未连接的数据报或流类套接口。如套接口未被捆绑,则系统赋给本地关联一个唯一的值,且设置套接口为已捆绑。请注意若名字结构中的地址域为全零的话,则connect()将返回WSAEADDRNOTAVAIL错误。
对于流类套接口(SOCK_STREAM类型),利用名字来与一个远程主机建立连接,一旦套接口调用成功返回,它就能收发数据了。对于数据报类套接口(SOCK_DGRAM类型),则设置成一个缺省的目的地址,并用它来进行后续的send()与recv()调用。
参数sockfd是一个由函数socket创建的套接字.如果该套接字的类型是SOCK_STREAM,则connect函数用于向服务器发送连接请求,服务器的IP地址和端口号由参数serv_addr指定.如果套接字的类型是SOCK_DGRAM, 则connect函数的好处是不必在每次发送和接受数据时都指定目的地址.
通常一个面向连接的套接字(如TCP套接字)只能调用一次connect函数,而对于无连接的套接字(如UDP套接字)则可以多次调用connect函数以改变与目的地址的帮顶.将参数serv_addr中的sa_family设置为AF_UNSPEC可取消绑定. 执行成功返回0,有错误则返回-1.
eg.该函数的常用方法如下:

struct sockaddr_in   serv_addr;
memset(&serv_addr, 0, sizeof(struct sockaddr_in));
serv_addr.sin_faminy = AF_INET;
serv_addr.sin_port = htons(80);//字节转换函数
if((inet_aton("172.17.242.131", &serv_addr.sin_addr) < 0);
{
    perror("inet_aton");
    exit(1);
}
if(connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0)
{
    perror("connect");
    exit(1);
}

5)在套接字上监听
socket()函数创建的socket默认是一个主动类型的,这种套接字可以用来主动请求连接到某个服务器(通过函数connect).但是作为服务器端的程序,通常在某个端口上监听等待来自客户端的连接请求,在服务器端,一般是先调用socket函数创建一个主动套接字,然后调用函数bind将套接字绑定到某个端口上,接着再调用函数listen将该套接字转化为监听套接字,等待来自客户端的连接请求。即listen函数将socket变为被动类型的,等待客户的连接请求。函数listen把套接字转化为被动监听【服务器端】,函数原型为:

#include<sys/socket.h>
int listen(int s, int backlog);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数——一般多个用户连接到一个服务器,服务器向这些客户提供某种服务,服务器端设置一个连接队列,记录已经建立的连接,参数badklog指定了该连接队列的最大长度.如果连接队列已经达到最大,之后的连接请求将被服务器拒绝. 执行成功返回0,发生错误返回-1。
注意:函数listen只是将套接字设置为倾听模式以等待连接请求,它并不能接受连接请求,真正接受客户端连接请求的是后面即将介绍的函数accept()。

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

#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);

参数sockfd是由函数socket创建,经函数bind绑定到本地的某一端口上,然后通过函数listen转化而来的监听套接字,用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。

参数addr用来保存发起连接请求的客户端连接端口,用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
参数addrlen是addr所指向的结构体的大小,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
返回值:如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。执行成功返回一个新的代表客户端的套接字,出错返回-1。

注意:
只能对面向连接的套接字使用accept函数.accept函数执行成功时,将创建一个新的套接字【连接套接字】,并且为这个新的套接字分配一个套接字描述符,并返回这个新的套接字描述符,这个新的套接字描述符与打开文件时返回的文件描述符类似,进程可以利用这个新的套接字描述符与客户端交换数据。
  参数sockfd所指的套接字描述符被设置为阻塞方式(linux下默认方式)且连接请求列队为空,则accept()将被阻塞直到有连接请求达到为止,如果参数sockfd所指的套接字被设置为非阻塞方式,则如果队列为空,accept立刻返回-1,errno 被设置为EAGAIN.
此时我们需要区分两种套接字:
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字);
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
使用两种套接字原因:如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。连接套接字 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号。
eg. 套接字为阻塞方式下该函数的常见用法:

int client_fd;
int client_len;
struct sockaddr_in client_addr;
//........socket() band() listen() 
client_len = sizeof(struct sockaddr_in);
client_fd = accept(sock_fd, (stuct sockaddr *)&client_addr, &client_len);
if(conn_fd < 0)
{
    perror("accept");
    exit(1);
}

7) read()、write()等函数
服务器与客户已经建立好连接了,则可以调用网络I/O进行读写操作了——网络不同进程之间的通信,常见网络I/O操作组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

  #include <unistd.h>

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

   #include <sys/types.h>
   #include <sys/socket.h>

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

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

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

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

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

#include <unistd.h>
int close(int fd);

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

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

  1. TCP套接字的数据传输
    (1)发送数据
    函数send()用来在TCP套接字上发送数据,函数原型为:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t send(int s , const void * msg, size_t len , int flags);

函数send只能对处于连接状态的套接字使用
参数:
s:已建立好连接的套接字描述符,即accept函数的返回值
msg: 指向存放带发送逐句的缓冲区
len : 待发送的数据长度.
flag:控制选项 一般设置为0或取以下宏:
MSG_OOB:在指定的套接字上发送带外数据(out-of-band-data),该类型的套接字必须支持带外数据(SOCK_STREAM).
MSG_ DONTROUTE:通过最直接的路径发送数据,而忽略下层协议的路由设置
如果要发送的数据太长而不能发送时,将出现错误,errno:EMSGSIZE,如果要发送的数据长度大于该套接字的缓冲区剩余空间大小时,send会一直被阻塞,如果该套接字被设置为非阻塞方式,此时立刻返回-1,error:EAGAIN.

返回值:
执行成功返回实际发送数据的字节数,出错返回-1.
套接字为阻塞方式下该函数的常见用法:

#define BUFFERSIZE 1500
char send_buf[BUFFERSIZE];
//..
if(send(conn_fd, send_buf, len, 0) < 0);
{
    perror("send");
    edxit(1);
}

(2)接收数据
函数recv 用来在TCP套接字上接受数据,函数原型为:

#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int s , void * buf , size_t len , int flags);

函数recv从参数s所指向的套接字描述符(必须是连接的套接字)上接收数据并保存到参数buf所指定的缓冲区,参数len 则为缓冲区长度.
参数说明:
socket:一个标识已连接套接口的描述字。
buf :用于接收数据的缓冲区。
len :缓冲区长度。
flags :指定调用方式。取值:
MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;
MSG_OOB 处理带外数据。
MSG_WAITALL:只有在接收缓冲区满时才返回
如果一个数据包太长以至于缓冲区不能完全放下时,剩余部分数据肯呢个将被丢放弃(根据接受数据的套接字类型而定).如果在指定的套接字上无数据到达时,recv()将被阻塞,如果该套接字被设置为非阻塞方式,则立即返回-1,函数recv接收到数据就返回,并不会等待接收到参数len指定的长度数据才返回。
返回值:若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
参数说明:
sockfd:标识一个已连接套接口的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:
(1)MSG_DONTWAIT:操作不会被阻塞;
(2)MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
(3)MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
(4)MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
(5)MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
(6)MSG_EOR:指示记录的结束,返回的数据完成一个记录。
(7)MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
(8)MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
(9)MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
(10)MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
from:(可选)指针,指向装有源地址的缓冲区。
fromlen:(可选)指针,指向from缓冲区长度值。

三、编程介绍

可参考:https://www.cnblogs.com/jiangzhaowei/p/8260971.html
https://www.cnblogs.com/hrers/p/11482913.html

服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。
/* File Name: server.c */  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
#define DEFAULT_PORT 8000  
#define MAXLINE 4096  
int main(int argc, char** argv)  
{  
    int    socket_fd, connect_fd;  
    struct sockaddr_in     servaddr;  
    char    buff[4096];  
    int     n;  
    //初始化Socket  
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){  
    printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //初始化  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT  
  
    //将本地地址绑定到所创建的套接字上  
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){  
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //开始监听是否有客户端连接  
    if( listen(socket_fd, 10) == -1){  
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    printf("======waiting for client's request======\n");  
    while(1){  
//阻塞直到有客户端连接,不然多浪费CPU资源。  
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){  
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);  
        continue;  
    }  
//接受客户端传过来的数据  
    n = recv(connect_fd, buff, MAXLINE, 0);  
//向客户端发送回应数据  
    if(!fork()){ /*紫禁城*/  
        if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)  
        perror("send error");  
        close(connect_fd);  
        exit(0);  
    }  
    buff[n] = '\0';  
    printf("recv msg from client: %s\n", buff);  
    close(connect_fd);  
    }  
    close(socket_fd);  
}  
 

 

客户端:

 

/* File Name: client.c */  
  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
  
#define MAXLINE 4096  
  
  
int main(int argc, char** argv)  
{  
    int    sockfd, n,rec_len;  
    char    recvline[4096], sendline[4096];  
    char    buf[MAXLINE];  
    struct sockaddr_in    servaddr;  
  
  
    if( argc != 2){  
    printf("usage: ./client <ipaddress>\n");  
    exit(0);  
    }  
  
  
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){  
    printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);  
    exit(0);  
    }  
  
  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(8000);  
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){  
    printf("inet_pton error for %s\n",argv[1]);  
    exit(0);  
    }  
  
  
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){  
    printf("connect error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
  
  
    printf("send msg to server: \n");  
    fgets(sendline, 4096, stdin);  
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)  
    {  
    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
    exit(0);  
    }  
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {  
       perror("recv error");  
       exit(1);  
    }  
    buf[rec_len]  = '\0';  
    printf("Received : %s ",buf);  
    close(sockfd);  
    exit(0);  
}  

inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。
int inet_pton(int af, const char src, void dst);//转换字符串到网络地址:
第一个参数af是地址族,转换后存在dst中
af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在
dst中
  af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在
dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。

四、网络字节序与主机字节序

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

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

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

网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。务必将其转化为网络字节序再赋给socket。

五、Socket中TCP的建立(三次握手)

TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手(three-way handshake),过程如下图所示。
三次握手

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
一个完整的三次握手也就是: 请求—应答—再次确认。
当客户端调用connect时,触发了连接请求,向服务器发送了SYN x包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN x包,调用accept函数接收请求向客户端发送SYN y ,ACK x+1,这时accept进入阻塞状态;客户端收到服务器的SYN y ,ACK x+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK y+1时,accept返回,至此三次握手完毕,连接建立。

我们可以通过网络抓包的查看具体的流程:
比如我们服务器开启9502的端口。使用tcpdump来抓包: tcpdump -iany tcp port 9502
然后我们使用telnet 127.0.0.1 9502开连接.:

telnet 127.0.0.1 9502 
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0  (2)
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0  (3)

14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

114:12:45.104687 时间带有精确到微妙
localhost.39870 > localhost.9502 表示通信的流向,39870是客户端,9502是服务器端
[S] 表示这是一个SYN请求
[S.] 表示这是一个SYN+ACK确认包:
[.] 表示这是一个ACT确认包, (client)SYN->(server)SYN->(client)ACT 就是3次握手过程
[P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推
[F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起
[R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接
win 4099 是指滑动窗口大小
length 18指数据包的大小
我们看到 (1)(2)(3)三步是建立tcp:
第一次握手:
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(syn=x)到服务器》
syn包(syn=x) : syn的seq= 2927179378 (x=2927179378)

第二次握手:
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,
收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包:
此时服务器主机自己的SYN:seq:y= syn seq 1721825043。
ACK为x+1 =(ack=x+1)=ack 2927179379

第三次握手:
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,
客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1)

客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accept,也进行3次握手完成。

六、TCP连接的终止(四次握手释放)

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:
四次挥手
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

1. 为什么连接的时候是三次握手,关闭的时候却是四次握手?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

2. 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

3. 为什么不能用两次握手进行连接?
3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

4. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值