赞
踩
举例理解:(唐僧西天取经)
在 IP 数据包头部中 有两个 IP 地址, 分别叫做源 IP 地址 和目的 IP 地址。
如果我们的台式机或者笔记本没有 IP 地址就无法上网,而因为每台主机都有 IP 地址,所以注定了数据从一台主机传输到另一台主机就一定有源 IP 地址和目的 IP 地址,所以在报头中就会包含源 IP 地址和目的 IP 地址。
而我们将数据从一台主机传递到另一台主机并不是目的,真正通信的其实是应用层上的软件。
而我们知道应用层可不止一个软件。既然有了公网 IP 标识了一台唯一的主机,那么数据就可以由一台主机传递到另一台主机。但是有这么多的软件(进程),如何保证软件 A 发送的被软件 B 接收呢?(用什么来标识主机上的进程的唯一性呢?)
端口号是用于标识在一台设备上运行的不同网络应用程序或服务的数字标识符,它是一个 16 位的数字,可以是 0~65535 之间的任意值。当一个应用程序或服务需要通过网络进行通信时,它会打开一个特定的端口,并侦听该端口上的连接,这个侦听过程称为绑定(binding)。当其他设备或应用程序尝试连接到此端口时,操作系统会将连接转发给已经绑定到该端口的应用程序进程。
因此,可以说端口号和进程之间存在一对一的映射关系。一个特定的端口号通常与一个特定的进程或应用程序相关联,而且一个端口号只能与绑定一个进程。当网络通信发生时,数据包会通过端口号被正确地路由至相应的进程,以确保通信的正确进行。
同一台设备上的不同进程可以绑定不同的端口号,这样就使得多个应用程序能够同时进行网络通信,而无需担心冲突。每个进程可以通过独立的端口号进行区分和识别,从而实现并发的网络通信。
为了更好的表示一台主机上服务进程的唯一性,规定用端口号标识服务进程、客户端进程的唯一性。端口号(port)是传输层协议的内容。
IP 地址(标识唯一主机)+ 端口号(标识唯一进程)能够标识网络上的某一台主机的某一个进程(全网唯一的进程)
端口号的解释:
每个端口号都有特定的作用和用途,例如常见的端口号有:
不是的,是手段。真正的网络通信过程,本质其实就是进程间通信。将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程。
前面说过进程间通信的本质是看到同一份资源,现在这个资源就是网络,而通信的本质就是 IO,因为我们上网的行为就两种情况:
- 首先 pid 是系统规定的,而 port 是网络规定的,这样就可以把系统和网络解耦。
- port 标识服务器的唯一性不能做任何改变,要让客户端能找到服务器,就像 110,120 一样不能被改变,而 pid 每次启动进程,pid 就会改变。
- 不是所有的进程都需要提供网络服务或请求(不需要 port),但每个进程都需要 pid。
虽然一个端口号只能绑定一个进程,但是一个进程可以绑定多个端口号。前面说了有源 IP 和目的 IP,而这里的 port 也有源端口号和目的端口号。我们在发送数据的时候也要把自己的 IP 和端口号发送过去,因为数据还要被发送回来,所以发送数据时一定会多出一部分数据(以协议的形式呈现)。
服务器内部已经内置好了。
传输层协议( TCP 和 UDP) 的数据段中有两个端口号 , 分别叫做源端口号和目的端口号, 就是在描述 “ 数据是谁发的, 要发给谁”。
我们用的套接字接口一定会使用传输层协议,不会绕过传输层去调用下面的协议。
传输层的协议分为 TCP 协议和 UDP 协议。
传输层就是用来解决可靠性的一个协议。
在网络通信中,现在的主流网络出现丢包的概率并不大。即使出现了丢包的情况,在有些场景下也是可以容忍的。
可不可靠在这里只是一个中性词,是他们的特点。可靠性是需要付出大量的编码和数据的处理成本的,往往在维护和编码上都比较复杂。而不可靠没有成本,使用起来也简单。所以二者要分场景使用。
- 小端:低权值的数放入低地址。
- 大端:低权值的数放入高地址。
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP 协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
- h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
- 例如 htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将 IP 地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
前面我们知道,IP + 端口号 port 标识了全网唯一的进程,我们把 IP + port 就叫做套接字socket。
套接字(Socket)是计算机网络编程中用于实现网络通信的一个抽象概念。它提供了一种编程接口,允许不同计算机之间通过网络进行数据传输和通信。具体来说,套接字可以看作是通信的两个端点,一个是服务器端的套接字,另一个是客户端的套接字。通过套接字,服务器端和客户端可以相互发送和接收数据。
在网络通信中,套接字使用网络协议(如 TCP/IP、UDP 等)来完成数据的传输和通信。根据所使用的网络协议的不同,套接字可以分为两种类型:
创建 socket 文件描述符(TCP/UDP, 客户端 + 服务器)
绑定端口号( TCP/UDP, 服务器)
开始监听 socket(TCP, 服务器)
接收请求( TCP, 服务器)
建立连接( TCP, 客户端)
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如:IPv4、IPv6,以及后面要讲的 UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
套接字有不少类型,常见的有三种:
三种应用场景:网络套接字主要运用于跨主机之间的通信,也能支持本地通信,而域间套接字只能在本地通信,而原始套接字可以跨过传输层(TCP/IP 协议)访问底层的数据。
为了方便,设计者只使用了一套接口,这样就可以通过不同的参数来解决所有的通信场景。这里举两个具体的套接字类型:sockaddr_in 和 sockaddr_un:
可以看到 sockaddr_in 和 sockaddr_un 是两个不同的通信场景,区分它们就用 16 地址类型协议家族的标识符。但是,这两个结构体都不用,我们用 sockaddr。
比方说我们想用网络通信,虽然参数是 const struct sockaddr *addr,但实际传递进去的却是 sockaddr_in 结构体(注意要强制类型转换)。在函数内部一视同仁,全部看成 sockaddr 类型,然后根据前两个字节判断到底是什么通信类型然后再强转回去。可以把 sockaddr 看成基类,把 sockaddr_in 和 sockaddr_un 看成派生类,构成了多态体系。
虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时, 使用的数据结构是 sockaddr_in, 这个结构里主要有三部分信息: 地址类型, 端口号, IP 地址。
in_addr 用来表示一个 IPv4 的 IP 地址,其实就是一个 32 位的整数。
要通信首先需要有 IP 地址,和绑定端口号:
在通信之前要先把网卡文件打开,函数作用:打开一个文件,把文件和网卡关联起来。
socket 是计算机网络提供的一个系统调用接口,它对传输层做了相关的一层文件系统级别的封装的接口。
(只需要关注上面框住的两个类,第一个 AF_UNIX 表示本地通信,而 AF_INET 表示网络通信)
这里我们讲的是 UDP,所以使用 SOCK_DGRAM。
从这里我们就联想到系统中的文件操作,以后的各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。
创建完套接字后我们还需要绑定 IP 和端口号。
所以我们要先定义一个 sockaddr_in 结构体填充数据,再传递进去。
点分十进制字符串风格的 IP 地址(例:"192.168.110.132" )每一个区域取值范围是 [0-255]:1字节 -> 4个区域。理论上,表示一个IP地址,其实4字节就够了。点分十进制字符串风格的 IP 地址 <-> 4字节。
- struct sockaddr_in {
- short int sin_family; // 地址族,一般为AF_INET或PF_INET
- unsigned short int sin_port; // 端口号,网络字节序
- struct in_addr sin_addr; // IP地址
- unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16
- };
创建结构体后要先清空数据(初始化),我们可以用 memset,系统也提供了接口:
填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题。
大小端转化接口:
- #include <arpa/inet.h>
- // 主机序列转网络序列
- uint32_t htonl(uint32_t hostlong);
- uint16_t htons(uint16_t hostshort);
- // 网络序列转主机序列
- uint32_t ntohl(uint32_t netlong);
- uint16_t ntohs(uint16_t netshort);
对于 IP,首先我们要先转成整数,再解决大小端问题。系统给了直接能解决这两个问题的接口:
上面的 inet_addr 就是把一个点分十进制的字符串转化成整数再进行大小端处理。
作为一款网络服务器,是永远不退出的。
服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了
首先要知道服务器要死循环,永远不退出,除非用户删除。站在操作系统的角度,服务器是常驻内存中的进程,而我们启动服务器的时候要传递进去 IP 和端口号。
看下面的第(5)点
- sockfd:从哪个套接字读。
- buf:数据放入的缓冲区。
- len:缓冲区长度。
- flags:读取方式。 0 代表阻塞式读取。
- src_addr 和 addrlen:输出型参数,返回对应的消息内容是从哪一个客户端发出的。第一个是自己定义的结构体,第二个是结构体长度。
现在我们想要知道是谁发送过来的消息,信息都被保存到了 peer 结构体中,我们知道 IP 信息在 peer.sin_addr.s_addr 中。首先这是一个网络序列,要转成主机序列,其次为了方便观察,要把它转换成点分十进制。
操作系统给了一个接口能够解决这两个问题:
man 手册上说,inet_ntoa 函数是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。
因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
- 在 APUE 中,明确提出 inet_ntoa 不是线程安全的函数。
- 但是在 centos7 上测试并没有出现问题,可能内部的实现加了互斥锁。
- 在多线程环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
同样获取端口号的时候也要由网络序列转成主机序列:
现在只需要等待用户端发送数据即可。
基于 IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示 32 位的 IP 地址,但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换:
其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr,因此函数接口是 void *addrptr。
首先我们要发送数据,就得知道客户端的 IP 和 port,而这里的 IP 就必须指明。
(这里的 IP 和 port 指的是要发送给谁)
创建套接字就跟前面的一样:
这里的客户端必须绑定 IP 和端口来表示主机唯一性和进程唯一性,但不需要显示的 bind。
因为服务器的端口号是大家都知道的,不能改变,如果变了就找不到服务器了。
而客户端只需要有就可以,只用标识唯一性即可。
举例:手机上有很多的 App,而每个服务端是一家公司写的,但是客户端却是多个公司写的。如果我们绑定了特定的端口,万一两个公司都用了同一个端口号呢?这样就直接冲突了。
所以操作系统会自动形成端口进行绑定(在发送数据的时候自动绑定),所以创建客户端我们只用创建套接字即可。
这里的参数和前面讲的 recvfrom 差不多,而这里的结构体内部需要自己填充目的 IP 和目的端口号。
要,但是一般 client 不会显示的 bind,程序员不会自己 bind。
client 是一个客户端 -> 普通人下载安装启动使用的 -> 如果程序员自己 bind 了-> client 一定 bind 了一个固定的 ip 和 port,那万一其他的客户端提前占用了这个 port 呢?
client 一般不需要显示的 bind 指定 port,而是让 OS 自动随机选择。
这里的 127.0.0.1 叫做本地环回。client 和 server 发送数据只在本地协议栈中进行数据流动,不会将我们的数据发送到网络中。
作用:用来做本地网络服务器代码测试的,意思就是如果我们绑定的 IP 是 127.0.0.1 的话,在应用层发送的消息不会进入物理层,也就不会发送出去。
当我们运行起来后想要查看网络情况就可以用指令 netstat,后边也可以附带参数:
我们可以发现,绑定不了云服务器上的公网 IP(也不建议绑定一个具体的 IP)。因为云服务器是虚拟化服务器(不是真实的 IP),所以不能直接绑定公网 IP。
答案是可以的,说明这个 IP 是属于这个服务器的,但如果这里不是一个内网的就无法找到。
实际上,一款服务器不建议指明一个同 IP。因为可能服务器有很多 IP,所以如果我们绑定了一个比如说 IP1,那么其他进程发送给 IP2 服务器就收不到了。
这里的 INADDR_ANY 实际上就是 0,这样绑定之后,再发送到这台主机上所有的数据,只要是访问绑定的端口(8080)的,服务器都能收到。这样就不会因为绑定了一个具体的 IP 而漏掉其他 IP 的信息了。其实就是让服务器在工作的工程中,可以从任意的 IP 中获取数据。
所以现在就不需要传递 IP 了:
- 执行 command -> pipe() fork() 子进程(exec*) command 命令。
- FILE *:可以将执行结果通过 FILE* 指针进行读取。
可以让别人通过公网 ip 给我这台机器发送消息,为了防止其他人对我的机器发送 rm 这类命令,加了一些限制(但并不完整!)
我们发送消息会经过服务端的转发,让每个在线的客户端都能看到发送的消息,这样就实现了群聊。
因为客户端不能立即收到消息打印出来(阻塞停留在接收消息),为了解决这个问题我们可以采用多线程,一个线程专门接收消息,一个线程专门发送消息。
可以发现,无论是多线程读还是写,用的 socket 都是一个,socket 代表的就是文件,UDP 是全双工的,可以同时进行收发而不受干扰。
在 Windows 下写客户端,在 Linux 下用 Linux 充当服务器实现客户端发送数据,服务器接收数据的功能(Windows 下的套接字和 Linux 下的几乎一样)。
注意:这里要实现正常通信,云服务器要进行被远程访问,就需要开放公网 IP 的端口,具体步骤可参考:【Linux】轻量级应用服务器如何开放端口 -- 详解-CSDN博客
下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。
在通信之前要先把网卡文件打开。
成功则返回打开的文件描述符(指向网卡文件),失败返回-1。
这个函数的作用是打开一个文件,把文件和网卡关联起来。
(只需要关注上面两个类,第一个 AF_UNIX 表示本地通信,而 AF_INET 表示网络通信。
(下面的内容用的是 SOCK_STREAM)
从这里我们就可以联想到系统中的文件操作,未来各种操作都要通过这个文件描述符,所以在服务端类中还需要一个成员变量表示文件描述符。
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用 bind 绑定一个固定的网络地址和端口号。
- bind() 的作用是将参数 sockfd 和 myaddr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号。
- 前面讲过,struct sockaddr* 是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度。
bind() 成功返回 0,失败返回 -1。
所以我们需要先定义一个address_in 结构体填充数据,再传递进去。
然后就是跟 UDP 一样,先初始化结构体,再处理 IP 和端口。要注意 IP 要绑定任意 IP,也就是 INADDR_ANY 。(前面有介绍原因)
因为 TCP 是面向连接的,当我们正式通信的时候,需要先建立连接,那么 TCP 跟 UDP 的不同在这里就体现了出来。要把 socket 套接字的状态设置为 listen 状态,只有这样才能一直获取新链接,接收新的链接请求。
举例帮助理解:我们买东西如果出现了问题会去找客服,如果客服不在,那么就无法回复我们,所以就规定了客服在工作的时候必须要时刻接收回复消息,那么这个客服所处的状态就叫做监听状态。
关于第二个参数:backlog,后边讲 TCP 协议参数时会再详细介绍,目前先直接用。( 一般不能太大,也不能太小)
static const int gbacklog = 10;
listen() 成功返回 0,失败返回 -1。
因为当前对应的文件描述符返回的套接字本身就是一个文件描述符,0、1、2 被占用,再创建一个文件,对应的就是 3。
前面我们有讲到,端口号用来标识该主机上的唯一的网络服务进程,也就是上面的 8080 代表的就是 tcp_server。同时,我们也说过,一个端口号是不能被被重复绑定的。
前面还讲到命令 netstat -anup,用来查看 udp server,现在我们用命令 netstat -antp 来查看 tcp server:
前面初始化完成,现在就是要开始运行服务端。TCP 不能直接发送数据,因为它是面向链接的,所以必须要先建立链接。
成功返回一个文件描述符,失败返回 -1。
- sockfd:文件描述符,找到套接字。
- addr:输入输出型参数,是一个结构体,用来获取客户端的信息。
- addrlen:输入输出型参数,客户端传过来的结构体大小。
我们的服务器程序结构是这样的:
举例帮助理解:我们去吃饭时,会发现一些店铺的门口有工作人员来招揽顾客,他将我们领进门店之后,他会站在门口继续招揽顾客,而我们会由里面的服务员来招待我们,给我们提供服务。
这里揽客的工作人员指的就是 sockfd,而店里面的服务员就是返回值的文件描述符。也就是说,sockfd 的作用就是把链接从底层获取上来,而返回值的作用就是跟客户端通信。
那么我们就知道了,成员变量中的 _sock 并不是通信用的套接字,而是获取链接的套接字。为了方便观察,我们可以把前面所有的 _sock 换成 _listensock。
上面获取到了通信用的套接字 sock,因为 TCP 通信是面向字节流的,所以后续通信全部是用文件操作(IO),因为文件也是面向字节流的。
IO 的操作可以封装一个函数,方便后续进行多次扩展:
当 IO 完之后要记得关闭文件描述符 sock,否则会导致可用描述符越来越少。
验证发现可以运行:
做一个测试,用命令 telnet(远程登陆工具)对服务端进行连接:
输入 Ctrl+],再回车,就可以发送消息并被对方收到了:
推出只需要输入命令 Ctrl+],再输入 quit 即可:
再启动一个客户端,尝试连接服务器,发现第二个客户端不能正确的和服务器进行通信。
当前版本只能一次处理一个客户端, 处理完一个才能处理下一个,这很显然是不能够被直接使用的。
因为上面的代码自始至终都是单进程的,是单进程获取链接成功,进行 service,而 service 里面是一个死循环,这个死循环就是在读取。也就是说,我们 accecpt 了一个请求之后,就在一直 while 循环尝试 read,没有继续调用到 accecpt。那么如果这个循环不退出,就一直保存正常读写。而它一直在读写,那我们的单进程执行流就无法回到前面继续调用 accept,从而导致不能获取新链接做处理。
因为 fork 后子进程会复制父进程的文件描述符。
这里注意子进程并不需要 _listensock 文件描述符,所以最好关闭。
如果父进程等待的话又会导致上面的情况,子进程不退出父进程就一直等待。子进程退出时,会给父进程发送一个 SIGCHLD,17 号信号。所以有一种解决办法就是用 signal 函数,在回调函数中把 waitpid 的参数设置为 -1(等待任意进程),就可以进行回收。
能。因为父进程在创建子进程时,子进程的文件描述符表会继承自于父进程,会和父进程看到同一份文件。
会。
不需要。子进程就是用来提供服务的,换而言之,子进程只需要知道 servicesock 就可以了,listensock 与它没有关系,尽量让进程关闭掉它所不需要的套接字。
Socket 可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入 Socket中,该 Socket 将这段信息发送给另外一个 Socket 中,使这段信息能传送到其他程序中。
所以客户端也需要一个套接字:
跟前面一样要绑定,但是不需要显示的 bind,但一定是需要 port,所以需要让 OS 自动进行 port 选择。客户端需要有连接别人的能力,也就是下面的(3)connect。
由于客户端不需要固定的端口号,因此不必调用 bind(),客户端的端口号由内核自动分配。
注意:
- 客户端不是不允许调用 bind(),只是没有必要调用 bind() 固定一个端口号,否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。
- 服务器也不是必须调用 bind(),但如果服务器不调用 bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
connect() 成功返回 0,出错返回 -1。
这里的 addr 和 addrlen 填入的是服务端信息。
在 UDP 通信中,客户端在 sendto 时会自动绑定 IP 和 port,而 TCP 就是在 connect 的时候进行绑定。因为 connect 是系统调用接口,所以在调用 connect 时会自动的给绑定当前客户端的 ip 和 port,进而可以让我们在后续使用 sockfd 进行通信。
前面我们在用了 signal 函数来对孙子进程可以回收,现在不用这种办法,可以这么写:
这里的意思就是创建孙子进程,父进程直接退出,让孙子进程执行 service,此时孙子进程就会被操作系统收养,不需要我们管,而父进程退出,外边的父进程也等待成功了。
其实右边服务端下面的等待可以不用等待,因为 SIGCHLD 信号默认的处理方式是忽略。
上面我们可以看到客户端退出了但是文件描述符并没有被回收,原因是我们只关闭了子进程的文件描述符,而没有关闭父进程。
不用。在多进程中每个文件都有文件描述符表,但在多线程中每一个线程和主线程共享一个文件描述符表,所以不能关闭文件描述符。
前面我们写过线程池,具体可以参考:【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解-CSDN博客
这里直接拿来用即可,修改一下代码,因为 service 可以不属于类,所以可以把 service 放在任务 Task.hpp 中。
上面的 change 函数的功能是:将小写字符转换成大写字符。
下图是基于 TCP 协议的客户端/服务器程序的一般流程:
- 调用 socket,创建文件描述符。
- 调用 bind,将当前的文件描述符和 ip/port 绑定在一起,如果这个端口已经被其他进程占用了,就会 bind 失败。
- 调用 listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的 accept 做好准备。
- 调用 accecpt,并阻塞,等待客户端连接过来。
- 调用 socket,创建文件描述符。
- 调用 connect,向服务器发起连接请求。
- connect 会发出 SYN 段并阻塞等待服务器应答(第一次)。
- 服务器收到客户端的 SYN,会应答一个 SYN-ACK 段表示 “同意建立连接”(第二次)。
- 客户端收到 SYN-ACK 后会从 connect() 返回,同时应答一个 ACK 段(第三次)。
- 建立连接后,TCP 协议提供全双工的通信服务。所谓全双工的意思是,在同一条连接中, 同一时刻,通信双方可以同时写数据。相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据。
- 服务器从 accept() 返回后立刻调用 read(),读 socket 就像读管道一样,如果没有数据到达就阻塞等待。
- 这时客户端调用 write() 发送请求给服务器, 服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间客户端调用 read()阻塞等待服务器的应答;
- 服务器调用 write() 将处理结果发回给客户端,再次调用 read() 阻塞等待下一条请求。
- 客户端收到后从 read() 返回,发送下一条请求,如此循环下去。
- 如果客户端没有更多的请求了,就调用 close() 关闭连接,客户端会向服务器发送 FIN 段(第一次)。
- 此时服务器收到 FIN 后,会回应一个 ACK,同时 read 会返回 0(第二次)。
- read 返回之后,服务器就知道客户端关闭了连接, 也调用 close 关闭连接,这个时候服务器会向客户端发送一个 FIN(第三次)。
- 客户端收到 FIN,再返回一个 ACK 给服务器(第四次)。
因为 TCP 是基于确定应答来保证单项可靠性的,如果对方给我发消息,我也给对方进行应答,那么就能够保证双向的可靠性。所以,发出去的断开连接的过程需要应答。当客户端断开连接时,要保证客户端到服务的连接被成功关闭,所以需要调用一次,而服务端除了要释放自身创建好的文件描述符,也要关闭从服务端到客户端对应的连接,因为双方都要调用 close() 各自两次,那么一来一来就绪各自需要两次挥手,加起来就是四次挥手。
- 应用程序调用某个 socket 函数时 TCP 协议层完成什么动作,比如调用 connect() 会发出 SYN 段。
- 应用程序如何知道 TCP 协议层的状态变化,比如从某个阻塞的 socket 函数返回就表明 TCP 协议收到了某些段,再比如 read() 返回 0 就表明收到了 FIN 段。
对比 UDP 服务器,TCP 服务器多了获取新链接和监听的操作,而因为 TCP 是面向字节流的,所以接收和发送数据都是 IO 操作,也就是文件操作。
- 可靠传输 VS 不可靠传输
- 有连接 VS 无连接
- 字节流 VS 数据报
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。