网络入门---TCP通信实现
目录标题
- 前言
- 准备工作
- tcpserver.hpp
- 构造函数
- 初始化函数(listen)
- 运行函数(accept)
- tcpserver.cc
- tcpclient.hpp
- 构造函数
- 初始化函数
- 运行函数(connect)
- tcpclient.cc
- 问题测试
- 改进一:多进程
- 改进二:多线程
- 改进三:线程池
- 完整代码
前言
在前面的文章中我们知道了如何使用UDP协议来进行通信,并且在单向通信基础上实现了单词翻译,指令执行,群聊系统,那么在本篇文章中我们将介绍该协议的好兄弟TCP协议,这两个协议都是传输层协议但是他们有着不同的性质,我们说TCP协议有链接的,UDP协议是无连接的,那这个有无连接体现在哪里呢?我们说TCP协议是面向字节流的而UDP协议是面向数据报的,那这两个协议在接收数据和发送数据的时候有没有什么区别呢?我们说UDP协议是不可靠的而TCP协议是可靠的,那这个可靠是通过更加复杂的实现换来的,那这个复杂体现在哪里呢?那本篇文章就将带着大家看看TCP协议是如何来实现网络通信,讲法与之前保持一致采用边实现程序边介绍TCP函数,因为UDP和TCP在实现通信的时候用很多的地方是相同的,所以在这些地方本篇文章不会详细介绍,建议大家先阅读一下之前的文章再来阅读本篇文章:UDP协议介绍。
准备工作
本篇文章要实现的程序就是用户端向服务端发送数据,然后服务端将数据打印在屏幕上并向客户端再发送一个数据作为回复,所以我们这里就创建4个文件,两个文件负责装载main函数用来形成可执行程序,两个文件用来装载描述客服端和服务端通信有关的类:
在客户端和服务端类中分别有构造函数析构函数初始化函数和运行函数,因为服务端对任意ip的消息都进行接受,所以服务端类只需要一个整形来接收套接字和一个16位的无符号整形来接收端口号,而客户端需要知道自己要向谁发送数据,所以服务端中得添加string类型的对象来保存客户端的ip地址还需要对应存储端口号的变量和套接字的变量,那么这两个文件中的内容就如下:
class tcpserver
{
public:tcpserver(){}void inittcpserver(){}void start(){}~tcpserver(){}
private:int _listensock;uint16_t _port;
};class tcpclient
{
public:tcpclient(){}void inittcpclient(){}void start(){}~tcpclient(){}
private:int _sock;uint16_t _serverport;string _serverip;
};
那么接下来我们首先就要实现tcpserver.hpp文件中的内容。
tcpserver.hpp
构造函数
因为服务端中有一个变量用来存储端口号,而socket套接字是使用函数socket函数来创建的,所以构造函数就只需要一个参数用来获取用户传递的端口号即可:
tcpserver(uint16_t port)
{}
那么在构造函数里面就将_port变量初始化一下,将套接字初始化为-1即可,那么这里的代码如下:
tcpserver(uint16_t port)
:_port(port)
,_listensock(-1)
{}
初始化函数(listen)
这里的逻辑大致与UDP协议是一样的,首先使用socket函数创建套接字:
第一个参数传递AF_INET表示使用网络通信,UDP在传递第二个参数的时候回传递SOCK_DGRM表示用户数据报套接字,那么TCP在传递第二个参数的时候则应该传递SOCK_STREAM表示流式套接字,第三个参数直接传递为0即可,然后返回值就是创建好的套接字也就是一个文件描述符,我们将返回值赋值给类中的套接字即可,因为该函数创建套接字的时候可能成功也可能失败,所以在赋值完成之后我们应该使用if语句检查一下,如果返回值为-1就表示创建套接字失败,我们就打印一下错误并使用exit函数退出程序:
void inittcpserver()
{//创建端口号_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);//枚举常量}cout<<"create socket success"<<endl;
}
套接字创建成功之后就该将主机的ip地址和对应的端口号与套接字绑定起来,那么这里就得使用bind函数:
在使用bind函数之前我们得先创建一个sockaddr_in对象,然后将对应的ip地址和端口号填写进去,再通过强制转换的方式将该对象的地址传递给bind函数,因为bing函数可能会出现失败,所以我们这里就创建一个变量用来记录bind函数的返回值,如果返回值为-1就说明创建失败我们就打印错误并退出程序:
void inittcpserver(){//创建端口号_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success"<<endl;//初始化信息struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY; //开始bind这些信息int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));if(n==-1){cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind success"<<endl;}
udp协议的初始化函数在写到这里的时候就已经结束了,但是tcp协议还需要做一件事情就是将当前套接字的状态设置为监听状态,处于监听状态的套接字可以收到客户端发来的链接请求反之则不能,这就好比客服上班了你才能咨询人工客服,客服下班你就只能咨询傻瓜机器人,那么这上班的状态就相当于是监听状态,那么这里用到的函数就是listen函数:
第一个参数表示将哪个套接字设置为监听状态,第二个参数大家展示不需要管传递一个不大的数字即可比如说5,同样的道理该设置监听状态也可能会出现的不成功的时候,那么就可以通过返回值来进行判断如果等于-1就表示设置监听状态不成功,那么该函数完整的代码如下:
void inittcpserver(){//创建端口号_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success"<<endl;//初始化信息struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY; //开始bind这些信息int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));if(n==-1){cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind success"<<endl;//将客户端设置为监听状态if(listen(_listensock,5)==-1){cout<<"listen socket error"<<endl;exit(LISTEN_ERR);}cout<<"listen socket success"<<endl;}
运行函数(accept)
listen函数的作用是将当前的套接字设置为监听状态,位于监听状态的套接字可以获取服务端发来的链接请求,但是listen函数是用来设置状态的不是用来接收链接请求的,要想获取连接就得使用函数accept:
该函数的第一个参数表示获取哪个套接字的链接请求,第二个参数和第三个参数就和recvfrom一样用来存储是哪个ip地址上的哪个端口号向这个套接字(第一个参数)发起了链接请求,如果该函数运行成功则该函数的返回值是一个文件描述符,如果该函数运行失败也就是获取链接失败则该函数返回-1,那么这里就存在一个问题在学习UDP的时候我们说套接字本身就是一个文件描述符,并且通过该文件描述符和recvfrom,sendto函数相结合就可以做到接收消息和发送消息的功能,那TCP这里的accept函数为什么还要返回一个文件描述呢?这个文件描述符又有什么用呢?那么这里我们就通过餐厅的例子来带着大家进行理解,首先餐厅是有很多的服务员的,有些服务员在餐厅内部进行工作他们负责给客户上菜倒水让客户吃的开心吃的舒心,但是有些服务员却是站在门口的,这些服务员往往长的都比较好看声音也非常的好听,那么她们干的事情就是招揽客户让客户前往自家店来进行用餐,我们把门外的服务员称为A,把餐厅内的服务员称为B,那么这里就存在一个生活现象,当客户被A招揽进餐厅到4号餐桌上吃饭时,是A来负责4号餐桌上的上下菜和卫生打理吗?很明显不是的,A将客户招揽进餐厅后往往都是叫B前来服务,当你在吃饭的时候B一直在服务你,而A则会去干她原本的事情也就是招揽其他的用户前来用餐,所以这里的服务员A就相当于之前设置为监听状态的套接字,而accept返回的文件描述符就相当于服务员B,所以参数中的套接字就负责获取向我申请链接的主机信息,一旦获取成功了就让返回的文件描述与其专门进行通信,而自己则继续获取其他想要和我链接的主机信息,那么这就是accept函数参数介绍,有了这个函数之后我们就可以尝试实现运行函数,首先该函数没有参数:
void start()
然后在函数里面我们就可以使用accept函数来获取链接,因为获取链接可能成功也可能失败,失败的话accept函数就会返回-1,所以这里就可以使用if语句来进行判断,又因为申请链接的客户端可能会有多个,所以这里就采用死循环的方式来不停的获取链接,有主机申请链接accept就会返回,没有的话就会阻塞等待,那么这里的代码如下:
void start()
{//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;}
}
链接获取成功之后就可以根据sock变量来进行通信,而通信可以看成一个单独的功能,所以这里又可以将通信的过程放到一个函数里面,未来我们想要不同的功能就调用的函数即可
void serverio(int sock)
因为这里是服务端所以首先创建一个数组作为缓冲区,然后创建一个死循环来不停的读取数据和发送数据:
void serverio(int sock)
{char buffer[1024];while(true){ }
}
UDP是面向数据报的所以他可以使用recvfrom函数读取数据使用sendto函数来发送数据,而这里的TCP是面向字节流并且文件的读写也是面向字节流的,所以这里可以采用与文件相同的方式来进行读取和发送也就是使用函数read和write,那么这里首先就是使用read函数从文件描述符sock中读取数据放到buffer中:
如果read函数读取成功该函数就会返回读取的字节,如果出现其他的情况导致了读取失败那么该函数就会返回-1,如果读到了文件的结尾该函数就会返回0比如说写段关闭,所以我们就可以根据该函数的返回值来判断客户端的链接是否中断:
void serverio(int sock)
{char buffer[1024];while(true){//把读取的数据当成字符串ssize_t n=read(sock,buffer,sizeof(buffer)-1);if(n>0){}else if(n==0){cout<<"client quit, me to"<<endl;break;}}
}
那么在if语句里面首先将下表为n的元素赋值为0以防读取到其他数据,然后我们就可与将缓冲区中的数据打印出来,创建一个string对象将其初始化为缓冲区中的内容并添加一句话表示这是客户端发来的消息,然后我们就可以通过write函数将string对象中的数据发送给sock对应的文件:
void serverio(int sock)
{char buffer[1024];while(true){//把读取的数据当成字符串ssize_t n=read(sock,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<"recv message: "<< buffer <<endl;string outbuffer =buffer;outbuffer+=" server[echo] ";write(sock,outbuffer.c_str(),outbuffer.size());}else if(n==0){cout<<"client quit, me to"<<endl;break;}}
}
那么这就是通信函数的内容,在start函数里面就可以直接调用该函数,该函数结束就表明此次网络通信也跟着结束了,那么这个时候就可以使用close函数将套接字sock对应的文件进行关闭,那么start函数完整的内容如下:
void start()
{//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;serverio(sock);close(sock);//下表的个数存在上限}
}
tcpserver.cc
该文件用来生成可执行程序,并且在生成可执行程序的时候得端口号,所以main函数得添加两个参数,然后在main函数开始的时候我们首先得判断一下参数的个数是否正确,如果不正确我们就执行一个函数来告诉使用者如何正确的执行该程序:
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " serverport\n\n";
}
int main(int args,char* argv[])
{if(args!=2){Usage(argv[0]);exit(1);}
}
然后就可以通过main函数的参数来获取端口号,并通过智能指针创建一个udpserver对象,然后就是调用初始化函数和start运行函数来执行程序:
#include"tcpserver.hpp"
#include<memory>
#include<stdlib.h>
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " serverport\n\n";
}
int main(int args,char* argv[])
{if(args!=2){Usage(argv[0]);exit(1);}uint16_t port=atoi(argv[1]);unique_ptr<tcpserver> tcil(new tcpserver(port));tcil->inittcpserver();tcil->start();return 0;
}
tcpclient.hpp
构造函数
运行客户端的时候得告诉用户你要连接的端口号和ip地址是多少,所以构造函数得有两个参数构造函数的内容就是初始化类中变量的值,原理和之前的一致,代码如下:
tcpclient(string serverip,uint16_t serverport)
:_serverport(serverport)
,_serverip(serverip)
,_sock(-1)
{}
初始化函数
有了前面的经验我们知道初始化函数的第一步就是创建套接字,并将socket函数的返回值赋值给类内成员,因为套接字可能会创建失败,所以这里得判断一下:
void inittcpclient()
{_sock=socket(AF_INET,SOCK_STREAM,0);if(_sock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);}
}
在服务端我们创建套接字之后还会通过函数bind将本主机的信息与套接字绑定起来,那这里需要吗?答案是需要的但是这件事不需要我们亲自来做而是操作系统替我们完成,因为操作系统做的话就可以避免端口号冲突的问题,所以我们这里就不需要调用bind函数,那这里我们需要将套接字设置为监听状态吗?答案也是不需要,因为这里实现的是客户端,客户端是用来申请链接的不是接收链接的,那你设置为监听状态有何用呢?对吧!所以到这里初始化函数就已经结束了。
运行函数(connect)
在发送消息之前我们首先得通过创建sockaddr_in对象的方式来告诉操作系统你要向哪个主机进行通信
void start()
{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(_serverport);server.sin_addr.s_addr=inet_addr(_serverip.c_str());
}
我们前面说TCP协议是有链接的在通信之前得先建立链接,所以服务端中先使用函数accept将套接字设置为监听状态再使用accept函数来获取链接,那么理所当然在客户端中就应该存在一个函数来向服务端申请链接,这个函数就是connect函数
第一个参数传递你要通过哪个套接字进行通信,第二个参数表示你要向哪个客户端申请链接,第三个参数就表示长度,如果链接申请成功该函数就会返回0,如果链接申请失败该函数就会返回-1:
void start()
{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(_serverport);server.sin_addr.s_addr=inet_addr(_serverip.c_str()); if(connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0){cout<<" socker connect error "<<endl;}else{}
}
链接申请成功之后我们就可以使用套接字来进行通信,那么这里也是和客户端相同的道理先创建一个循环,然后通过write函数和套接字_sock将信息发送出去,再使用read函数和套接字_sock读取服务端发来的数据,那么该函数完整的代码如下:
void start()
{struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(_serverport);server.sin_addr.s_addr=inet_addr(_serverip.c_str()); if(connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0){cout<<" socker connect error "<<endl;}else{string message;while(true){cout<<"please enter# ";getline(cin,message);write(_sock,message.c_str(),message.size());char buffer[1024];int n=read(_sock,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<"server回显: "<<buffer<<endl;}else if(n==0){break;}} }
}
tcpclient.cc
服务端的main函数和客户端的差不多这里就不多说了,直接看代码:
#include"tcpclient.hpp"
#include<memory>
#include<stdlib.h>
static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
int main(int args,char*argv[])
{if(args!=3){Usage(argv[0]);exit(1);}string ip=argv[1];uint16_t port=atoi(argv[2]);unique_ptr<tcpclient> tcil(new tcpclient(ip,port));tcil->inittcpclient();tcil->start();return 0;
}
问题测试
首先创建两个渠道一个客户端一个服务端:
然后客户端发送一个hello,我们就可以看到服务端也显示了一个hello,并且客户端还接收到了服务端发来的回应:
通过运行结果我们不难发现在一个服务端场景下上面的实现是没有问题的,那么接下来我们再创建一个渠道让其运行服务端看看会有什么现象:
可以看到这里让我们输入消息但是消息发送之后没有得到任何的回应,并且客户端上也没有显示2号服务端发送的消息:
我们将一号客户端退出再看看会有什么样的现象:
退出之后可以看到服务端上立马显示出来之前2号客户端上发送的消息:
并且2号客户端上也立马显示出来服务端发送过来消息回应:
那么这就说明当前写的代码面对多个服务端时会出现问题,而这个问题就出现在服务端的start函数:
void start()
{//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;serverio(sock);close(sock);//下表的个数存在上限}
}
void serverio(int sock)
{char buffer[1024];while(true){//把读取的数据当成字符串ssize_t n=read(sock,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<"recv message: "<< buffer <<endl;string outbuffer =buffer;outbuffer+=" server[echo] ";write(sock,outbuffer.c_str(),outbuffer.size());}else if(n==0){cout<<"client quit, me to"<<endl;break;}}
}
服务端通过死循环不停的获取客户端发过来的链接请求,获取链接之后就会运行serverio函数来完成通信,可是该函数内部又是一个死循环来不停的接收消息和发送消息,所以这就导致了外层循环运行了一次之后就再也无法运行第二次除非之前链接成功的客户端主动断开,导致read函数返回0进而退出死循环,那么这就是上面出现问题的原因,所以接下来我们就采用三种方式来改进这个问题。
改进一:多进程
在学习操作系统的时候我们提到过fork函数,该函数可以帮助我们创建子进程然后通过fork函数的不同返回值从而让父子进程执行不同的内容,那么这里我们就有了一个思路,我们可以创建子进程让子进程执行serverio函数,然后父进程继续获取新链接这样两者不就互不耽误了嘛!首先使用fork创建子进程再使用if语句让父子进程执行不同的内容:
pid_t id =fork();
if(id==0)
{//子进程执行
}
//父进程执行
fork在创建子进程的时候会将PCB,虚拟地址空间,页表等等等都拷贝一边,所以子进程也继承了很多父进程的内容,其中就包括了父进程打开的_listensock文件描述符,但是子进程是不需要这个文件描述符的,所以这里最好将其关闭,以免影响该文件的正常关闭从而导致资源浪费,_listensock关闭之后就可以调用serverio函数调用完成之后就可以关闭sock套接字,然后调用exit该函数进行退出,那么这就是子进程要执行的代码
pid_t id =fork();
if(id==0)
{//子进程执行close(_listensock);serverio(sock);close(sock);exit(0);
}
//父进程执行
因为父进程不需要通过sock套接字来完成通信,所以父进程首先关闭该套接字,然后对子进程的信息进行回收,这里的回收就可以使用waitpid函数,但是在回收的时候就会出现问题,是采用阻塞等待还是采用非阻塞式等待呢?如果是阻塞等待话我们上面的操作就好像脱裤子放屁多此一举啊,对吧!为了不让主进程等待客户端断开链接,所以我们创建了父子进程,如果采用阻塞等待话这里的父进程不就又要等了嘛!等的话又怎么能建立新链接呢?所以有小伙伴就想到了使用非阻塞等待,可是这样的话又会出现一个新的问题,一次等待不可能将所有的进程全部回收掉那如果在次之后再也没有建立新的链接的话,accept函数不就一直没有返回了吗?那这个时候非阻塞等待也失去了意义,所以不管采用哪个都存在问题,所以我们这里的解决方法就是让子进程再创建一个孙子进程,让孙子进程执行serverio函数然后子进程直接退出,这时的孙子进程就是一个孤儿进程,当他运行结束的时候会由操作系统来进行回收,这样就解决了父进程等待的问题,那么这里的代码如下:
void start(){//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;pid_t id =fork();if(id==0){//子进程执行close(_listensock);if(fork>0){//子进程exit(0)}//孙子进程serverio(sock);close(sock);exit(0);}//父进程执行close(sock);//下表的个数存在上限}}
那么接下来就可以进行测试,首先将服务端运行起来再将两个客户端运行起来,我们就可以看到下面这样的现象:
然后两个客户端同时输入消息也不会出现问题
那么这是多进程的一种解决方法,跟这相同道理的还有这样写的:signal(SIGCHLD,SIG IGN);
子进程在结束的时候会发送型号SIGCHID,signal(SIGCHLD, SIG_IGN) 中的 SIGCHLD 是代表子进程状态变化的信号,SIG_IGN 是一个宏,表示忽略该信号。这行代码告诉操作系统在父进程中忽略(不处理)收到的子进程结束的信号。通常情况下,父进程可能会调用 wait() 或 waitpid() 来等待子进程的终止,并获取其状态,但使用 SIG_IGN 则表示父进程不关心子进程的终止状态,子进程结束后会被操作系统回收资源,那么这里的写法就变成下面这样:
void start()
{signal(SIGCHLD,SIG_IGN);//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;pid_t id =fork();if(id==0){//子进程执行close(_listensock);serverio(sock);close(sock);exit(0);}//父进程执行close(sock);//下表的个数存在上限}
}
改进二:多线程
既然可以采用多进程的方式来进行改进,那么同样的道理也可以采用多线程的方式来进行改进,首先创建一个pthread_t类型的变量,然后再使用pthread_create函数创建线程,因为创建线程的时候得告诉新线程你要执行的函数是哪个?所以我们这里创建一个返回值为void参数为void类型的函数,在该函数里面执行serverio函数,在主线程里面就对新线程进行join回收,因为join是阻塞等待并且他没有非阻塞等待,所以这里就采用分离线程的方式来进行处理,新线程运行结束之后就自动被操作系统回收无需父进程处理,所以这里还得用到detach函数,但是这里还存在一个问题类中静态函数是无法访问类中非静态函数的,这一pthread_create函数在传递参数的时候可以传递this指针过去这样在函数里面就可以强制类型转换然后就可以正常的访问调用了,可是这里还存在一个问题函数的调用是需要传递参数的,而这个参数是之前start函数中通过accept函数返回的也就是变量sock,那如何将这个参数也传递过去呢?所以这里就可以再创建一个名为ThreadData的类,该类中有两个变量专门用来存储通信所用的文件描述符和this指针,那么这里的代码就如下:
class tcpserver;
class ThreadData
{
public:ThreadData(tcpserver* self,int& sock):_self(self),_sock(sock){}
private:tcpserver *_self;int _sock;
};
新线程执行的函数threadRoutine的实现就是先进行线程分离,再进行参数转换,然后通过参数调用serverio函数,最后关闭文件描述符释放之前创建的ThreadData对象,那么这里的代码如下:
static void* threadRoute(void* args){pthread_detach(pthread_self());ThreadData* td=static_cast<ThreadData*>(args);td->_self->serverio(td->_sock);close(td->_sock);delete td;return nullptr;}
那么start函数的内容就如下:
void start()
{signal(SIGCHLD,SIG IGN);//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;pthread_t tid;ThreadData* td=new ThreadData(this,sock);pthread_create(&tid,nullptr,threadRoute,td);}
}
那么这就是多线程的修改方式。
改进三:线程池
在前面的文章中我们实现过一个线程池,这里就直接将代码拷贝到当前路径下:
#include "Thread.hpp"
#include "LockGuard.hpp"
#include "log.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>
using namespace ThreadNs;
const int gnum = 10;
template <class T>
class ThreadPool;
template <class T>
class ThreadData
{
public:ThreadPool<T> *threadpool;std::string name;public:ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){}
};
template <class T>
class ThreadPool
{
private:static void *handlerTask(void *args){ThreadData<T> *td = (ThreadData<T> *)args;while (true){T t;{LockGuard lockguard(td->threadpool->mutex());while (td->threadpool->isQueueEmpty()){td->threadpool->threadWait();}t = td->threadpool->pop(); // pop的本质,是将任务从公共队列中,拿到当前线程自己独立的栈中}t();}delete td;return nullptr;}ThreadPool(const int &num = gnum) : _num(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 0; i < _num; i++){_threads.push_back(new Thread());}}void operator=(const ThreadPool &) = delete;ThreadPool(const ThreadPool &) = delete;public:void lockQueue() { pthread_mutex_lock(&_mutex); }void unlockQueue() { pthread_mutex_unlock(&_mutex); }bool isQueueEmpty() { return _task_queue.empty(); }void threadWait() { pthread_cond_wait(&_cond, &_mutex); }T pop(){T t = _task_queue.front();_task_queue.pop();return t;}pthread_mutex_t *mutex(){return &_mutex;}public:void run(){for (const auto &t : _threads){ThreadData<T> *td = new ThreadData<T>(this, t->threadname());t->start(handlerTask, td);std::cout << t->threadname() << " start ..." << std::endl;}}void push(const T &in){LockGuard lockguard(&_mutex);_task_queue.push(in);pthread_cond_signal(&_cond);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);for (const auto &t : _threads)delete t;}static ThreadPool<T> *getInstance(){if (nullptr == tp){_singlock.lock();if (nullptr == tp){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;}
private:int _num;std::vector<Thread *> _threads;std::queue<T> _task_queue;pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *tp;static std::mutex _singlock;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template <class T>
std::mutex ThreadPool<T>::_singlock;
该线程池是使用单例模式实现的,并且在实现的过程中还用到了我们自己封装的线程和自己封装的锁,所以我们还得将这些代码也复制到当前路径下,这是线程的封装:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>namespace ThreadNs
{typedef std::function<void *(void *)> func_t;const int num = 1024;class Thread{private:// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为staticstatic void *start_routine(void *args) // 类内成员,有缺省参数!{Thread *_this = static_cast<Thread *>(args);return _this->callback();}public:Thread(){char namebuffer[num];snprintf(namebuffer, sizeof namebuffer, "thread-%d", threadnum++);name_ = namebuffer;}void start(func_t func, void *args = nullptr){func_ = func;args_ = args;int n = pthread_create(&tid_, nullptr, start_routine, this); // TODOassert(n == 0); (void)n;}void join(){int n = pthread_join(tid_, nullptr);assert(n == 0);(void)n;}std::string threadname(){return name_;}~Thread(){// do nothing}void *callback() { return func_(args_);}private:std::string name_;func_t func_;void *args_;pthread_t tid_;static int threadnum;};int Thread::threadnum = 1;
} // end namespace ThreadNs
这是锁的封装:
#pragma once#include <iostream>
#include <pthread.h>
class Mutex
{
public:Mutex(pthread_mutex_t *lock_p = nullptr): lock_p_(lock_p){}void lock(){if(lock_p_) pthread_mutex_lock(lock_p_);}void unlock(){if(lock_p_) pthread_mutex_unlock(lock_p_);}~Mutex(){}
private:pthread_mutex_t *lock_p_;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex): mutex_(mutex){mutex_.lock(); //在构造函数中进行加锁}~LockGuard(){mutex_.unlock(); //在析构函数中进行解锁}
private:Mutex mutex_;
};
线程池是通过懒汉模式来进行实现的,所以每次都通过getInstance函数来获取对象,所以在函数start里面先通过getInstance函数和run函数让线程池运行起来然后再构建新连接,连接创建成功之后我们就可以通过线程池中的push函数往池中添加任务,所以在通信之前我们还得构建任务,通过线程池中的handlerTask我们也不难发现这个任务内有函数调用的运算符重载所以我们这里就再创建一个文件里面就装有任务构建的类,因为任务就是网络通信,网络通信需要serverio函数和对应的套接字,所以该类就两个成员变量和对应重载函数和构造函数
class Task
{using func_t = std::function<void(int)>;public:Task(){}Task(int sock, func_t func): _sock(sock), _callback(func){}void operator()(){_callback(_sock);}
private:int _sock;func_t _callback;
};
因为该文件用来构造方法,而方法又和网络通信有关所以我们这里可以直接将serverio有关,所以这里就直接将serverio也搬到该文件里面:
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <functional>
void serviceIO(int sock)
{char buffer[1024];while (true){ssize_t n = read(sock, buffer, sizeof(buffer) - 1);if (n > 0){// 目前我们把读到的数据当成字符串, 截止目前buffer[n] = 0;std::cout << "recv message: " << buffer << std::endl;std::string outbuffer = buffer;outbuffer += " server[echo]";write(sock, outbuffer.c_str(), outbuffer.size()); // 多路转接}else if (n == 0){// 代表client退出cout<<"client quit, me too!";break;}}close(sock);
}class Task
{using func_t = std::function<void(int)>;public:Task(){}Task(int sock, func_t func): _sock(sock), _callback(func){}void operator()(){_callback(_sock);}private:int _sock;func_t _callback;
};
最后start函数调里面掉用push函数在里面传递临时的任务对象即可,那么start函数的完整代码如下:
void start()
{ThreadPool<Task>::getInstance()->run();//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;//线程池中添加任务ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));}
}
代码的运行结果如下:
完整代码
tcpserver.hpp的代码如下:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include<signal.h>
#include<unistd.h>
#include<string>
#include<pthread.h>
#include"ThreadPool.hpp"
#include"Task.hpp"using namespace std;
enum{SOCKET_ERR=1,BIND_ERR,LISTEN_ERR,
};
// class tcpserver;
// class ThreadData
// {
// public:
// ThreadData( tcpserver* self, int& sock)
// :_self(self)
// ,_sock(sock)
// {}
// tcpserver *_self;
// int _sock;
// };
class tcpserver
{
public:tcpserver(uint16_t port):_port(port),_listensock(-1){}void inittcpserver(){//创建端口号_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);}cout<<"create socket success"<<endl;//初始化信息struct sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(_port);local.sin_addr.s_addr=INADDR_ANY; //开始bind这些信息int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));if(n==-1){cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;exit(BIND_ERR);}cout<<"bind success"<<endl;//将客户端设置为监听状态if(listen(_listensock,5)==-1){cout<<"listen socket error"<<endl;exit(LISTEN_ERR);}cout<<"listen socket success"<<endl;}//多进程写法一// void start()// {// //服务端不停的获取新的链接// for(;;)// {// struct sockaddr_in peer;// socklen_t len=sizeof(peer);// int sock=accept(_listensock,(struct sockaddr*)&peer,&len);// if(sock==-1)// {// cout<<"accept error"<<endl;// continue;// }// cout<<"accept a new link success"<<endl;// cout<<"sock : "<<sock<<endl;// pid_t id =fork();// if(id==0)// {// //子进程执行// close(_listensock);// if(fork>0)// {// //子进程// exit(0)// }// //孙子进程// serverio(sock);// close(sock);// exit(0);// }// //父进程执行// close(sock);//下表的个数存在上限// }// }//多进程写法二// void start()// {// signal(SIGCHLD,SIG IGN);// //服务端不停的获取新的链接// for(;;)// {// struct sockaddr_in peer;// socklen_t len=sizeof(peer);// int sock=accept(_listensock,(struct sockaddr*)&peer,&len);// if(sock==-1)// {// cout<<"accept error"<<endl;// continue;// }// cout<<"accept a new link success"<<endl;// cout<<"sock : "<<sock<<endl;// pid_t id =fork();// if(id==0)// {// //子进程执行// close(_listensock);// serverio(sock);// close(sock);// exit(0);// }// //父进程执行// close(sock);//下表的个数存在上限// }// }//多线程版// void start()// {// signal(SIGCHLD,SIG_IGN);// //服务端不停的获取新的链接// for(;;)// {// struct sockaddr_in peer;// socklen_t len=sizeof(peer);// int sock=accept(_listensock,(struct sockaddr*)&peer,&len);// if(sock==-1)// {// cout<<"accept error"<<endl;// continue;// }// cout<<"accept a new link success"<<endl;// cout<<"sock : "<<sock<<endl;// pthread_t tid;// ThreadData* td=new ThreadData(this,sock);// pthread_create(&tid,nullptr,threadRoute,td);// }// }// static void* threadRoute(void* args)// {// pthread_detach(pthread_self());// ThreadData* td=static_cast<ThreadData*>(args);// td->_self->serverio(td->_sock);// close(td->_sock);// delete td;// return nullptr;// }void start(){ThreadPool<Task>::getInstance()->run();//服务端不停的获取新的链接for(;;){struct sockaddr_in peer;socklen_t len=sizeof(peer);int sock=accept(_listensock,(struct sockaddr*)&peer,&len);if(sock==-1){cout<<"accept error"<<endl;continue;}cout<<"accept a new link success"<<endl;cout<<"sock : "<<sock<<endl;//线程池中添加任务ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));}}~tcpserver(){}
private:int _listensock;uint16_t _port;
};
tcpclient.hpp的完整代码如下:
#include<iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unistd.h>
#include<string>
#include<string.h>
using namespace std;
enum{SOCKET_ERR=1,
};
class tcpclient
{
public:tcpclient(string serverip,uint16_t serverport):_serverport(serverport),_serverip(serverip),_sock(-1){}void inittcpclient(){_sock=socket(AF_INET,SOCK_STREAM,0);if(_sock==-1){cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;exit(SOCKET_ERR);}}void start(){struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(_serverport);server.sin_addr.s_addr=inet_addr(_serverip.c_str()); if(connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0){cout<<" socker connect error "<<endl;}else{string message;while(true){cout<<"please enter# ";getline(cin,message);write(_sock,message.c_str(),message.size());char buffer[1024];int n=read(_sock,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<"server回显: "<<buffer<<endl;}else if(n==0){break;}} }}~tcpclient(){}
private:int _sock;uint16_t _serverport;string _serverip;
};
相关文章:

网络入门---TCP通信实现
目录标题 前言准备工作 tcpserver.hpp构造函数初始化函数(listen)运行函数(accept) tcpserver.cctcpclient.hpp构造函数初始化函数运行函数(connect) tcpclient.cc问题测试改进一:多进程改进二:多线程改进三:线程池完整代码 前言 在前面的文…...

neuq-acm预备队训练week 8 P2661 [NOIP2015 提高组] 信息传递
题目背景 NOIP2015 Day1T2 题目描述 有 n 个同学(编号为 1 到n)正在玩一个信息传递的游戏。在游戏里每人都有一个固定的信息传递对象,其中,编号为 i 的同学的信息传递对象是编号为 Ti 的同学。 游戏开始时,每人都…...

《C++新经典设计模式》之第18章 备忘录模式
《C新经典设计模式》之第18章 备忘录模式 备忘录模式.cpp 备忘录模式.cpp #include <iostream> #include <vector> #include <memory> using namespace std;// 保存对象内部状态,必要时恢复 // 在不破坏封装性的前提下,捕获对象的内部…...

OWASP安全练习靶场juice shop-更新中
Juice Shop是用Node.js,Express和Angular编写的。这是第一个 完全用 JavaScript 编写的应用程序,列在 OWASP VWA 目录中。 该应用程序包含大量不同的黑客挑战 用户应该利用底层的困难 漏洞。黑客攻击进度在记分板上跟踪。 找到这个记分牌实际上是&#…...

当使用RSA加密,从手机前端到服务器后端的请求数据存在+
将转成了空格,导致解密出错 将空格转成了...

BUUCTF crypto做题记录(3)新手向
目录 一、Rabbit 二、篱笆墙的影子 三、丢失的MD5 四、Alice与Bob 一、Rabbit 得到的密文:U2FsdGVkX1/ydnDPowGbjjJXhZxm2MP2AgI 依旧是看不懂是什么编码,上网搜索,在侧栏发现Rabbit解码,直接搜索就能有在线解码网站 二、篱笆…...

SpringMVC修炼之旅(2)基础入门
一、第一个程序 1.1环境配置 略 1.2代码实现 package com.itheima.controller;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;//定义…...

matlab 最小二乘拟合空间直线(方法二)
目录 一、算法原理1、算法过程2、参考文献二、代码实现三、结果展示四、相关链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫与GPT。 一、算法原理...

ASPICE-汽车软件开发能力评级
Automotive SPICE(简称A-SPICE 或 ASPICE),全称是“Automotive Software Process Improvement and Capacity dEtermination”,即“汽车软件过程改进及能力评定”模型框架。 常被用于评估一家汽车软件供应商的软件开发能力&#x…...

准确!!!在 CentOS 8 上配置 PostgreSQL 14 的主从复制
在 CentOS 8 上配置 PostgreSQL 14 的主从复制,并设置 WAL 归档到特定路径 /home/postgres/archive 的步骤如下: 主服务器配置(主机) 配置 PostgreSQL: 编辑 postgresql.conf 文件: vim /data/postgres/p…...

leetcode 1466
leetcode 1466 使用dfs 遍历图结构 如图 node 4 -> node 0 -> node 1 因为节点数是n, 边长数量是n-1。所以如果是从0出发的路线,都需要修改,反之,如果是通向0的节点,例如节点4,则把节点4当作父节点的节点&…...

想学编程,但不知道从哪里学起,应该怎么办?
怎样学习任何一种编程语言 我将教你怎样学习任何一种你将来可能要学习的编程语言。本书的章节是基于我和很多程序员学习编程的经历组织的,下面是我通常遵循的流程。 1.找到关于这种编程语言的书或介绍性读物。 2.通读这本书,把…...

Python数据科学视频讲解:Python概述
2.1 Python概述 视频为《Python数据科学应用从入门到精通》张甜 杨维忠 清华大学出版社一书的随书赠送视频讲解2.1节内容。本书已正式出版上市,当当、京东、淘宝等平台热销中,搜索书名即可。内容涵盖数据科学应用的全流程,包括数据科学应用和…...

数据结构之内部排序
目录 7-1 直接插入排序 输入格式: 输出格式: 输入样例: 输出样例: 7-2 寻找大富翁 输入格式: 输出格式: 输入样例: 输出样例: 7-3 PAT排名汇总 输入格式: 输出格式: 输入样例: 输出样例: 7-4 点赞狂魔 输入格式: 输出格式: 输入样例&a…...

软考高级备考-系统架构师(机考后新版教材的备考过程与资料分享)
软考高级-系统架构设计师 考试复盘1.考试结果2.备考计划3.个人心得 资料分享 考试复盘 1.考试结果 三科压线过,真是太太太太太太太幸运了。上天对我如此眷顾,那不得不分享下我的备考过程以及一些备考资料,帮助更多小伙伴通过考试。 2.备考…...

Spring Boot 整合kafka:生产者ack机制和消费者AckMode消费模式、手动提交ACK
目录 生产者ack机制消费者ack模式手动提交ACK 生产者ack机制 Kafka 生产者的 ACK 机制指的是生产者在发送消息后,对消息副本的确认机制。ACK 机制可以帮助生产者确保消息被成功写入 Kafka 集群中的多个副本,并在需要时获取确认信息。 Kafka 提供了三种…...

Java+Swing: 主界面组件布局 整理9
说明:这篇博客是在上一篇的基础上的,因为上一篇已经将界面的框架搭好了,这篇主要是将里面的组件完善。 分为三个部分,北边的组件、中间的组件、南边的组件 // 放置北边的组件layoutNorth(contentPane);// 放置中间的 Jtablelayou…...

pytorch:YOLOV1的pytorch实现
pytorch:YOLOV1的pytorch实现 注:本篇仅为学习记录、学习笔记,请谨慎参考,如果有错误请评论指出。 参考: 动手学习深度学习pytorch版——从零开始实现YOLOv1 目标检测模型YOLO-V1损失函数详解 3.1 YOLO系列理论合集(Y…...

YOLOv8配置文件yolov8.yaml解读
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 | 接辅导、项目定制 位置 该文件的位置位于 ./ultralytics/cfg/models/v8/yolov8.yaml 模型参数配置 # Parameters nc: 80 # number of classes scales: #…...

4-Tornado高并发原理
核心原理就是协程epoll事件循环,再使用协程之后,开销是特别的小,那具体如何提供高并发的呢? 异步非阻塞IO 这意味我们整套开发的模式不在与原来一样,正因为不再一样,所以有时我们在理解代码时就有可能会比…...

基于以太坊的智能合约开发Solidity(事件日志篇)
//声明版本号(程序中的版本号要和编译器版本号一致) pragma solidity ^0.5.17; //合约 contract EventTest {//状态变量uint public Variable;//构造函数constructor() public{Variable 100;}event ValueChanged(uint newValue); //事件声明event Log(…...

【BME2112】w11 notes
下周做老鼠实验 group analysis SPM group analysis 数据地址resting state 可以分析:correlation 计算两个脑区的相关性 静息态实验简单functional 成功的实验能看到激活区不成功的实验:比如被试头动太大,不是健康的被试 Spontaneous brain…...

Flutter笔记:滑块及其实现分析1
Flutter笔记 滑块分析1 作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 :291148484163.com 本文地址:https://blog.csdn.net/qq_28550263/article/details/134900784 本文从设计角度&#…...

【React Hooks】useReducer()
useReducer 的三个参数是可选的,默认就是initialState,如果在调用的时候传递第三个参数那么他就会改变为你传递的参数,实际开发不建议这样写。会增加代码的不可读性。 使用方法: 必须将 useReducer 的第一个参数(函数…...

如何把kubernetes pod中的文件拷贝到宿主机上或者把宿主机上文件拷贝到kubernetes pod中
1. 创建一个 Kubernetes Pod 首先,下面是一个示例Pod的定义文件(pod.yaml): cat > nginx.yaml << EOF apiVersion: v1 kind: Pod metadata:name: my-nginx spec:containers:- name: nginximage: nginx EOF kubectl app…...

Android 13 - Media框架(20)- ACodec(二)
这一节开始我们就来学习 ACodec 的实现 1、创建 ACodec ACodec 是在 MediaCodec 中创建的,这里先贴出创建部分的代码: mCodec mGetCodecBase(name, owner);if (mCodec NULL) {ALOGE("Getting codec base with name %s (owner%s) failed", n…...

TCP单聊和UDP群聊
TCP协议单聊 服务端: import java.awt.BorderLayout; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.V…...

智能优化算法应用:基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.鲸鱼算法4.实验参数设定5.算法结果6.参考文献7.MA…...

TortoiseGit 小乌龟svn客户端软件查看仓库地址
进入代码路径...

uniapp微信小程序分包,小程序分包
前言,都知道我是一个后端开发、所以今天来写一下uniapp。 起因是美工给我的切图太大,微信小程序不让了,在网上找了一大堆分包的文章,我心思我照着写的啊,怎么就一直报错呢? 错误原因 tabBar的页面被我放在分…...