引言:
北京时间:2023/8/25/15:52,昨天刚把耗时3天左右的文章更新,充分说明我们这几天并不是在摆烂中度过,而是在为了更文不懈奋斗,历时这么多天主要是因为该部分知识比较陌生,所以需要我们花费大量的时间去细细研究,为后面无论是TCP套接字,还是网络的学习都能更加融会贯通。并且这几天在闲暇时间把《一念永恒》听了一下,目前还在过渡期,不过根据一些伏笔我认为小高潮即将来临,根据前期的一些内容我意识到为什么该小说能被动漫公司拍成动漫,主要应该是因为耳根对于主角前期的角色塑造相比于其它小说来说更加独具匠心,以人性最朴实的长生、怕死为目的,再配上各种小心思和漫不经心为行文规律,很好的就将大众在心目中对人性的理解给刻画出来,再加上耳根独到的幽默理解,可以说一个完美的小说主角就被呈现出来了。这也可能就是耳根能屹立网文巅峰的原因吧!相比那些爽文,其实也不错,但是相比于这种角色刻画更深的小说,差距还是非常大的,白金就是白金,虽然套路单一,但是文笔确实无可挑剔!ok,不谈了,该篇博客我们承接上篇博客有关UDP套接字的知识,来看一看有关TCP套接字相关的知识,并根据TCP套接字实现一份TCP版本的网络通信客户端和服务端。
深入套接字编程
在上篇博客中我们重点对socket编程中的sockaddr结构体进行了深入理解,并且对有关socket编程的接口进行了详细认识,最后结合sockaddr_in结构体和有关接口实现了三种不同场景的UDP服务端/客户端,并成功让其完成数据的接收和传输,真正意义上实现了一份基于UDP协议的socket网络通信代码。但由于时间以及内容问题,上篇博客我们只讲解了基于UDP版本的socket网络通信,所以该篇博客就让我们一起来看看如何使用socket编程实现一份TCP版本的服务端/客户端,从而实现基于TCP协议的socket网络通信代码。
理解TCP版本的套接字接口
在上篇博客中,我们对套接字常见API进行了详解介绍,但准确的来说我们介绍的大部分都是基于UDP版本下的套接字接口,所以此时对于TCP版本来说,我们需要补充几个在TCP版本下套接字编程中会被使用到的接口,其中为什么在TCP版本下需要新增不同的套接字接口,本质原因非常简单,也就是基于TCP特点带来区别,因为TCP需要满足可靠性传输,事先建立连接,面向字节流等原则。所以接下来我们就来看看TCP版本套接字编程的新增接口吧!
-
int listen(int sockfd, int backlog);
功能:将套接字设置为监听状态(服务器处于阻塞),用于监听指定端口号客户端的连接请求,并且维护一个监听队列/等待队列用于存储向该服务端发送的连接请求,最终将该监听队列提供给accept接口使用,从而实现服务端和客服端之间的连接。换一个角度理解,也就是说服务器代码在执行时会被阻塞在该处(监听),从而一直处于监听状态,只有当某客户端发送连接请求被监听到(存储在监听队列)之后,代码才会继续向后执行,从而让accept接口完成连接。第一个参数sockfd(重新理解sockfd): 在创建套接字时返回的一个套接字描述符,虽然之前我们一直称其为文件描述符,但更准确应该称为套接字描述符,同理存储在对应的文件描述符表上,是一种套接字的引用(套接字的标识符),也就是可通过该套接字描述符访问到套接字,从而可以对套接字进行管理和操作,最终让套接字实现不同的功能,完成对应的工作(接口)。第二个参数backlog:同理上述所说,该参数与监听队列有关,用于表示监听队列存储客户端连接请求的上限(也就是个数)。返回值:同理监听成功返回0,监听失败返回-1且错误码被设置。 -
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:同理上述listen接口中所说,该接口用于与指定端口号客户端建立连接,直接从监听队列中获取客户端请求,注意:在客户端使用connect接口发送连接请求时,并不会直接将客户端的端口号和IP地址发送给服务端,而将端口号和IP地址发送给服务端的这个过程一般是系统依据TCP协议完成,所以最终accept接口再根据TCP协议对监听队列中对客户端发送的请求连接进行解包,从而获取客户端IP地址和端口号的同时,也获取到服务端的IP地址和端口号(客户端发送),从而让客户端和服务端建立连接。第一个参数sockfd:同理套接字描述符,用于对指定套接字进行accept操作,第二个参数addr:同理作为输出型参数,接收客户端的IP地址和端口号,让服务端可以显式的打印和识别客户端的IP地址和端口号,第三个参数addrlen:同理表示sockaddr_in结构体的长度,也就是大小。返回值:注意,此时这个返回值是一个新的套接字描述符,也就是说在accept接口中一定也存在socket接口的使用,具体为什么需要返回新的文件描述符,而不是像UDP版本一样使用一个套接字文件描述符就能实现网络通信,下述讲解。 -
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:同理上述accept接口中所说,在客户端中使用,向服务端发送连接请求,当然基于此时我们对listen和accept接口的理解,明白,此时connect接口发送的请求肯定是先被listen监听到,监听成功之后才被accept处理,尝试建立连接,并且对于connect接口来说,并不是每次一申请就能成功,一般需要多次申请,因为有很多原因会导致失败,如:监听队列满了的情况,然后同理就是该客户端向指定服务端发送请求,肯定是需要知道该服务端的IP地址和端口号,所以connect接口在发送请求时,就会把服务端的IP地址和端口号发送过去,这也就是为什么accept接口默认不仅知道客户端IP地址和端口号,还知道服务端IP地址和端口号的原因。第一个参数sockfd:同理套接字描述符,用于对指定套接字进行connect操作,第二个参数addr:同理,表示客户端需要知道向那个服务端发送连接请求,表示服务端的IP地址和端口号,第三个参数addrlen:同理表示服务端sockaddr_in的长度。返回值:同理成功返回0,失败返回-1且错误码被设置。 -
ssize_t read(int fd, void *buf, size_t count);
功能:首先明白在当初学习系统编程之文件系统相关知识时,对该接口有过一定的了解,该接口就是用来从特定的文件描述符中读取数据,同理此时对于套接字描述符而言,read接口同样可以从其读取数据,第一个参数fd:同理套接字描述符,向指定套接字中读取数据,第二个参数buffer:作为输出型参数,存储被读取数据,第三个参数count:读取数据的字节数,返回值:同理返回实际读取数据的字节数。 -
ssize_t write(int fd, const void *buf, size_t count);
功能:同理,该接口用来向特定的文件描述符中写入数据,在socket编程中也就是向套接字描述符中写入数据,第一个参数fd:同理套接字描述符,第二个参数buffer:存储需要写入数据,第三个参数count:写入数据的大小。返回值:同理写入数据的实际字节数。
行文来到此处,对于有关TCP版本新增的套接字接口我们就大致有了一定了解,但是根据上述所说,此时我们还需要解决一个问题,也就是为什么accept接口将服务端和客户端建立连接之后,需要返回一个新的套接字描述符,而不是像UDP版本一样,使用一个套接字描述符就能实现,本质也就是我想强调出TCP与UDP实现网络通信之间的区别,所以在特别区分UDP和TCP不同版本的套接字通信之前,此时我们回顾一下UDP版本下的套接字通信,第一步:同理创建套接字,获取套接字描述符,第二步:初始化sockaddr_in结构体,并将其绑定到内核套接字中,第三步:使用sendto和recvfrom这类多参数接口用于指定目标地址以及接收目标地址,最终成功实现客户端和服务端之间的数据传输。可发现在UDP版本下实现套接字通信相比于TCP需要使用listen、accept和connect等接口来说较为容易,而根据上述对接口的分析以及功能,TCP版本下的套接字通信,不仅需要对服务端套接字监听,且还需要对监听队列中的客户端连接请求建立联系,然后在建立连接之后才允许进行数据间传输,所以对于TCP套接字来说,此时就不仅仅只是像UDP一样完成数据间传输就行,重点在于它还需要对服务端和客户端建立连接,所以同理对于listen接口和accept接口来说,它们本质都需要直接作用于套接字描述符,从而让套接字具备监听和建立连接的能力,且又因为套接字还需要具备数据传输的能力,所以如果一个套接字监听到客户端请求,完成连接,进行数据传输时,此时其它客户端就不能再使用该套接字与该服务端建立连接,因为此时对于服务端来说,它唯一的套接字此时正在完成数据传输工作,所以这样就会导致服务端的效率非常低下,不能并发处理多个客户端,所以最终为了解决这一问题,此时在accpet接口完成服务端与客户端的连接之后,就需要返回一个新的套接字描述符,本质也就是向操作系统再申请一个网络通信请求并分配一定的资源(空间资源等),只有这样,才能让每个客户端在与该服务端建立连接之后都拥有一个属于自己的套接字,本质理解也就是让服务端不再像UDP版本一样,整份代码只有唯一的套接字,而是多个套接字,每与一个客户端建立连接,就有一个新的套接字供其使用,让服务端在建立连接和传输数据之间无冲突。
注意: 虽然TCP协议需要满足可靠性传输、建立连接、面向字节流等特性,但并不意味着上述接口就能实现这些特性,如TCP协议需要满足可靠性传输,而想要实现可靠性传输则需要通过一系列机制,如:确认应答、重传、流量控制和拥塞控制等… ,这些机制的实现都是体现在TCP协议当中,也就是说我们使用套接字编程实现TCP版本的网络通信,本质只需要让系统识别这是一份TCP协议版本的代码,并且编码符合TCP的这三大特性就行,本质也就是因为这些特性都是TCP协议规定好的,我们只需要遵从,这也就是为什么我们在编码时使用read和write接口作为来进行数据传输,因为其具有面向字节流的特性。同理再次强调,套接字的本质就是一种网络通信机制,socket创建套接字的本质也就是向操作系统申请进行网络通信请求,向操作系统分配资源的一个过程,并且使用各种接口对套接字进行操作,本质就是一种对网络通信功能的实现与控制而已。
正式进入TCP套接字代码编写
在正式进入代码编写前,我们需要先明白几个点,首先在上述对TCP套接字接口有了一定理解,此时我们明白TCP套接字相对于UDP套接字而言,关键就在于在数据传输前让服务端与客户端建立连接,所以我们对accept接口进行着重讲解,明白为什么其返回值是一个新的套接字描述符,但,此时我们要明白,无论是UDP版本的服务端还是TCP版本的服务端,如果我们不对其进行多执行流的控制,那么它们无论如何都不支持客户端的并发访问,所以就算是TCP套接字中accept会返回一个新的套接字描述符,它也无法同时进行数据传输和与客户端建立连接,本质就是因为单执行流必须按照顺序执行代码,明白这点之后,此时对于TCP套接字网络通信我们依然分为三个场景,单执行流服务端、父子进程服务端、多线程及线程池服务端。
首先是客户端的实现
同理,客户端代码不存在特殊改动,所以此时客户端代码我们只举例一份,如下所示:
第一个场景:单执行流实现
重点强调
: 根据上述代码及其注解,我们需要重点明白两点,一点是在某客户端向服务端发送连接请求之前,该服务端是在listen接口处处于阻塞状态,而不是accept接口处,二点是accept接口中自带数据发送和接收功能,也就是recvfrom和sendto接口的实现,并且本质建立连接这个概念就是在内部对recvfrom和sendto接口进行封装,因为accept接口天生具有获取客户端地址信息和服务端地址信息的能力(客户端会将服务端的地址信息也传过来),从外部理解也就是在服务端与客户端之间建立一个双向通道通信,而对于返回一个新的套接字描述符的本质也就可以理解为返回accept接口中使用socket接口专门为recvfrom和sendto接口创建的那个套接字描述符。
第二个场景:父子进程实现服务端
对于使用父子进程来实现多执行流解决服务端并发访问问题,从上述代码来看非常合理,唯一的缺点就是创建进程带来的效率消耗问题,本质也就是该服务端除了会为每一个客户端创建一个新套接字描述符之外,此时还需要为每一个客户端创建一个子进程,所以这也就是为什么我们需要使用多线程方法来改善的原因。当然这部分知识下述讲到,这里我重点想要回顾一下父进程回收子进程的问题,上述代码是一种设计套路,并不是经典的我们学过的回收子进程的方法,我们学过的父进程的回收子进程的方法一共有三种,第一种是直接使用waitpid接口,但因为此时我们的目的是子进程处理客户端数据的同时,父进程同时也可以去调用accept接口,让另一个客户端和服务端建立连接,所以我们不能让父进程处于阻塞式等待,所以在使用waitpid接口时,应该采用非阻塞式等待, waitpid(id, nullptr, WNOHANG);
但最后因为需要防止父进程在执行accept接口时,监听队列中没有客户端的请求连接,而导致父进程被阻塞在accept接口处,无法执行waitpid接口回收子进程,所以这个方法不推荐在服务端上使用,第二种是使用子进程退出信号回收子进程,因为子进程退出时会向父进程发送一个退出信号SIGCHLD,所以我们只需要在父进程收到该信号时,让父进程将该信号的信号递达方式设置为忽略,此时系统自动会帮父进程回收该子进程, signal(SIGCHLD, SIG_IGN);
第三种同理是使用子进程退出信号SIGCHLD处理,在父进程收到该信号时,让父进程将该信号的默认处理方式设置为waitpid接口,实现子进程的回收,signal(SIGCHLD, handler);
明白了上述有关子进程回收相关知识,对于使用父子进程实现服务端的知识,我们就理解到这,本质没有难度,重点就在于子进程的回收而已。
第三个场景:多线程实现服务端
以上代码就是将服务端以多线程的形式实现多执行流来解决并发访问问题,本质对比父子进程实现多执行流,好处就在于不需要频繁创建进程,提高服务端效率,编码关键就在于新建线程参数控制,其它过程同理本质就是为了实现多执行流,一个负责处理客户端数据,一个负责建立连接。当然因为我们学习了线程池相关的知识,所以对于这种临时创建线程的方法,我们依然可以使用线程池的方法来改进,具体下篇博客详解,主要是因为还需要重点讲解有关日志方面的知识,所以需要留一份代码来看看日志具体如何使用。