本篇文章,继续和大家分享与网络相关的知识。本次的内容主要会涉及到TCP客户端的模拟实现,TCP服务器的模拟实现和守护进程等相关知识。
服务器
tcp和udp创建套接字,bind的步骤是一样的。
makefile
Main.cc
TcpServer.hpp
TCP是面向连接的服务器,在正式通信之前,一定得随时随地的等待别人连接。也就是说通信之前,得先建立连接。
如果一直没有人连接,TCP服务器就会一直处于等待的状态,这种状态,我们称之为listen状态。
进入listen状态需要使用函数listen函数。
listen函数
listen函数的第一个参数是套接字,第二个参数表示全连接的长度,这个后面解释。
有连接来了,我们怎么获取,需要使用函数accept。
accept函数
accept函数的返回值是一个文件描述符。它的第一个参数是套接字,后面两个参数用于了解是谁连接了我。
我们把Start函数完善一下:
这里多了一个套接字,为了便于区分,把原来类内的套接字改成listensock_。
为什么TCP这里多了一个套接字呢?通信的时候,为什么用这个而不是那个?
讲个小故事,你就明白了。一个餐厅里,有一个店员叫张三,一个李四。张三负责在餐厅外揽客。揽到客人后,就引到餐厅里,交由李四来协助点菜,自己则继续到餐厅外揽客。
监听用的套接字,就相当于张三,只负责监听,不提供其他服务。而accept返回的套接字则负责服务。
我们现在还没有写客户端,怎么测试服务器呢?可以使用telnet进行指定服务的远程登录,说是登录,其实就是连接。
TCP这里能不能直接绑定云服务器公网IP呢?不能。
我们可以简单提供一个Service服务,进行测试:
由于read和write是面向字节流的,所以TCP通信不需要另外设置接口,直接用write和read就可以了。网络通信,规定使用大端。为什么发过去的消息,不需要转网络序列呢?因为你使用的接口,或默认把,套接字正常通信的内容,由主机序列转成网络序列。
编译运行,使用telnet指令进行连接,就可以进行通信了。
客户端
客户端要获取服务端的服务,首先得建立连接。怎么建立连接?使用connect函数。
connect函数
connect的第一个参数是套接字,后两个参数说明向那个服务器获取服务。
TcpClient.cc
改善后的服务器:
TcpServer.hpp
编译运行,就能让客户端和服务端通信起来了。
改善服务器
当我们打开两个客户端同时请求服务时,就会发现,只有一个客户端能获取服务。
这是什么原因?这是因为我们的Start是单进程版的。
我们改成多进程版的,才能进行通信处理多个连接。
像下面这样改可以吗?不可以,主进程在等待进程的时候,会阻塞住。这不还是只能,处理一个服务吗?那怎么办呢?一种方式是采用捕获信号的方式回收。
另一种方式是,让孙子进程提供服务。这种方式,子进程创建孙子进程后,马上退出被回收,主进程就不会阻塞住了。而孙子进程变成孤儿进程被领养,会自动回收。
编译运行,就能同时进行多个服务了。
上面这种方式,我们把监听套接字关闭了,会不会有问题?不会
创建一个进程的成本太高了,我们可以采用成本较小的线程。
编译运行,我们就能进行通信了。
说到线程,还记得我们之前做的线程池吗?
我们还可以使用线程池来提供服务,由于线程池的线程数量有限,我们只能提供短连接。
TcpServer.hpp
Task.hpp
ThreadPool.
编译运行,通信一次,服务就停止了。
简单的翻译
我们可以基于刚刚线程池版的服务,做一个简单的翻译功能。
一,先写一个文本。
在写一个加载字典的头文件Init.hpp
Task.hpp
编译运行,就能使用客户端获取翻译服务了。
读端问题
write函数,也会有出错的时候,我们也要对write函数进行出错处理。
我们这里可以故意向100号文件写入。
100号文件不存在,write出错返回了,符合预期。
还有一种情况,如果读端关掉了,写端向管道里写。OS就会发送sigpipe信号把写端杀掉。
如果我们不想因为这种异常终止服务器,可以忽略此信号。
我们平时玩游戏的时候,如果遇到网络不好,就会掉线,然后需要掉线重连。掉线重连是怎么做到的呢?
下面,我们给客户端增加一个掉线重连功能。
TcpClient.cc
编译运行,在客户端连上服务器后,断连,就会进行重连了。
服务端终止后,无法立即重新启动,如果需要立即重新启动,需要用到函数setsockopt。
这里暂时先使用,后面再讲解。
编译运行,让客户端连上服务端后,终止服务端,再重启服务端,就能看到断线重连的过程了。
守护进程
我们使用xshell进行登录。每次用户登录,就会创建一个会话框,称为session。一个会话框只有一个前台进程,但有多个后台进程。
什么是前台进程,什么又是后台进程呢?谁拥有键盘文件,谁就是前台进程。前台进程,会一直从标准输入获取数据,而后台进程不行。
那怎么创建后台进程呢?运行程序的时候,再其后面加上取地址符号。
我们可以使用jobs指令查看后台任务,成列出来的信息中,最前面的数字叫任务号。
我们可以是fg+任务号的方式,将后台任务提到前台。
fg+任务号
后台任务向显示器输出信息,并不会影响指令的正常接受,只不过回显的时候是乱序而已。将后台任务提到前台后,就可以使用Ctrl+C的方式终止
如果我们使用Ctrl+Z将一个前台进程暂停了,前台进程会自动放到后台。为什么会自动呢?因为命令行中,要一直存在前台进程,接受键盘的数据,实施对会话框的操作。
[common_108@iZj6cdwczct2c4sl0aulbpZ ~]$ bg 1
我们可以使用bg+任务号指令,将暂停的程序唤醒。
这里我们启动两个后台任务。一个是./process,另一个是sleep 1000 | sleep 2000 | sleep 3000。
启动任务后,可以使用如下指令查看:
[common_108@iZj6cdwczct2c4sl0aulbpZ ~]$ ps ajx | head -1 && ps ajx | grep -Ei 'process|sleep'
PGID是进程组ID的意思,单独启动的程序会自成一个进程组。每个进程组会有一个组长。如果一个进程组只有一个进程,那它就是这个组的组长。如果一个组里有多个进程,这些进程中,第一个启动的就是组长。
进程关系,不仅可以是进程独立,还可以是进程之间构成一组。
进程组和任务有什么关系呢?以前的进程就是任务。一个任务可以指派给一个进程或者多个进程完成。
不要叫前台进程和后台进程,更喜欢叫前台任务和后台任务。
多个任务在同一个session内启动的sid是一样的。
我们打开两个会话窗口后,使用如下指令查看,就会发现它们两个的SID是不同的。
[common_108@iZj6cdwczct2c4sl0aulbpZ ~]$ ps ajx | head -1 && ps ajx | grep -E '\-bash$'
我们关掉xshell,重新登录,会发现后台进程不会关掉,但会收到用户登陆和退出的影响。
如果不想受到任何用户登录和注销的影响,就需要守护进程化。我们把自成进程组、自成会话的进程,称之为守护进程。
注销就是把整个会话全部关掉,包括后台任务。
如何变成守护进程?
将一个进程变成守护进程,需要使用函数setsid,让进程组ID变成会话ID。调用这个函数的进程,不能是组长。
启动一个程序,自己就是组长,怎么让自己不是组长?让自己不是第一个进程,就不是组长了。代码上怎么操作?很简单,创建一个子进程,让子进程继续往后执行,而父进程终止退出。父进程终止退出,那子进程不就变成了孤儿进程了吗?是的,所以守护进程的本质是孤儿进程。
我们这里将守护进程化,封装成一个函数,这样调用它的进程,就会变成守护进程。
守护进程化后,进程是一直运行在后台的,我们不需要信息向屏幕打印。
有一个文件叫/dev/null
写入/dev/null中的内容,会被自动丢弃。
Daemon.hpp
TcpServer.hpp
启动服务器后,退出登陆。
再次登录xshell,我们依然能获取服务。
我之所以xshell登录Linux,是因为OS默认会启动一个端口为22的服务。通常守护进程都是以d结尾的。
其实,守护进程化有现成的函数。
TCP建立连接的过程会进行三次握手,释放连接的时候,会进行四次挥手。
TCP通信时全双工的,发送和接受使用的是两个不同的缓冲区。
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和网络相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。