【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)
- [Tcp套接字编程]
- 一.服务器端进程:
- 1.创建套接字
- 2.绑定网络信息
- 3.设置监听状态
- 4.获取新连接
- 5.根据新连接进行通信
- 二.客户端进程:
- 1.创建套接字
- 2.连接服务器套接字
- 3.连接成功后进行通信
- 三.version1:单进程版
- 四.version2:多进程版
- 五.version3:多线程版
- 六.version4:线程池版
- 七.解决细节问题:完善服务器和客户端
- 1.write写入会存在偶发性失败,进程被信号杀死
- 2.客户端每次请求都需要重新发起连接
- 3.服务器端出现问题,客户端需要尝试自救
- 4.复用ip地址和端口号
[Tcp套接字编程]
一般使用网络套接字编程时都需要引用以上头文件。
一.服务器端进程:
我们首先先进行服务器端的编写:服务器端肯定是需要知道有自己的ip地址和端口号的。
1.创建套接字
跟Udp套接字编程一样,通信前需要创建套接字,创建套接字的本质就是打开一个网络文件。
要注意的是Tcp是面向字节流的,创建套接字时,按照字节流形式创建。
//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);
2.绑定网络信息
创建完套接字后,也就是打开一个网络文件,我们需要绑定该服务器的网络信息,比如ip地址和端口号等。这样该套接字才能找到服务器端。客户端往该套接字写入时,服务器端就能从该套接字里读取到。
服务器往该套接字里写入时,连接该套接字的客户端就能接收到。
//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功
将字符粗类型转int类型使用inet_aton()调用,简单的那个存在线程安全。
3.设置监听状态
Tcp与Udp不同之处在于,Tcp是面向连接的,什么意思呢?就是在真正通信前,需要先建立连接才能正式通信。比如Udp在绑定完网络信息后,就可以直接进行通信了,但Tcp不行,Tcp在绑定完网络信息后,还需要建立连接。
Tcp相比较Udp比较被动,在通信之前,需要随时随地地等待别人连接上去才能进行通信。所以服务器要一直处于一个等待到来连接的状态。
而等待到新连接需要一种能力,这个能力就是监听!只有我们将套接字设置为监听状态,该套接字才能获取到新连接。不然获取不到别人的连接。
//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连
4.获取新连接
以上都是服务器的一些网络信息初始化步骤,接下来才是启动服务器。服务器启动服务呢?
首先服务器在通信之前需要获取到客户端的连接。这样才可以和客户端进行通信。那么如何获取到客户端的连接呢?
1.前提是该服务器的套接字已经设置为监听状态,只有套接字设置为监听状态才可以获取到客户端发起的连接。
2.利用accept函数就可以获取到客户端发起的连接。
3.accept函数和recvfrom函数类似,它从套接字里获取到连接,不仅仅可以获取到客户端发起的连接,还可以获取到客户端的网络信息。而获取到客户端的网络信息是通过两个输出型参数带出来的。
4.注意accept函数的返回值是一个文件描述符。为什么呢?也就是accept返回的也是一个套接字文件。accept从一个套接字文件里获取到连接然后再返回套接字文件?什么意思呢?
【问题】:为什么会有两个套接字?
accept从我们创建的套接字里获取连接,结果又返回一个套接字文件,为什么呢?这个新的套接字是干啥的?
1.也就是曾经被创建的,被绑定网络信息的,设置为监听状态的套接字是专门用来获取客户端发起的连接的,然后将该连接返回给一个新的套接字,让服务器通过新的套接字与客户端通信,而原来的套接字就又处于监听状态,继续来监听客户端发起的新连接。
2.其实就是为了提高通信效率,一个套接字既去获取连接,又去通信,效率比较低,而让一个套接字一直处于监听状态,获取到新连接后,就转让给另一个套接字,服务器其实是通过accept返回的套接字与连接端的客户进行通信的。
3.未来,获取的连接会越来越多,也就是返回的套接字会越来越多,而监听的套接字只有一个。
lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));}
将客户端网络信息带出来注意需要将网络字节序转换成用户字节序。
还有我们的传输的内容会自动转换的。
5.根据新连接进行通信
一旦服务器端获取到新连接成功后,就可以与对端的客户通过返回的套接字进行通信。
而Tcp是面向字节流的,面向字节流就可以直接使用文件操作,因为文件操作也是面向字节流的,也就是文件操作里的read,write就可以往套接字里读取和写入。
1.我们就可以根据获取到的套接字与客户端进行通信。
2.我们可以将该工作分离成一个函数service。与客户端通信需要知道连接的套接字和客户端的网络信息。
void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}
1.当服务器获取到连接后,read读取时,如果读取成功就会返回读取的个数大小。
2.当服务器获取到连接后,read读取之前或者连接又全部断掉,那么read就会读取到0。也就是在read的时候,没有用户连接了,那么服务器就没有必要再从该套接字里读取了。那么服务器读取的工作就退出,不再读取了。后续更需要将该套接字关闭起来。因为没有人使用了,就需要关闭起来。
3.read读取失败时就会返回小于0.
二.客户端进程:
服务器端基本工作已经准备好,接下来就是客户端,客户端在创建完套接字后需要绑定网络信息吗?
1.需要!但不需要显示绑定,操作系统会帮我们自动绑定。
2.客户端在发起连接时,系统就会帮我们自动绑定。
客户端想和服务器连接,那么用户肯定是知道该服务器端的套接字信息的,不然无法进行通信。所以在用户进行通信之前,我们首先将服务器端的网络信息给处理下。
比如将服务器的ip字符粗类型转换成int类型
有一下三个函数可以使用:
std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);
1.创建套接字
不管是服务器端还是客户端要想网络通信都需要创建套接字,根据通信类型,创建Tcp套接字还是Udp套接字根据选择。我们这里创建Tcp套接字,按照面向字节流创建:
//创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?
2.连接服务器套接字
创建完套接字后该干什么呢?因为用户已经知道服务器端的套接字信息,所以我们就根据服务器端的套接字信息来向服务器端发起连接。
因为Tcp是面向连接的,所以必须先发起连接,对方获取到连接才能进行通信。
//连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的int n=connect(sockfd,(struct sockaddr*)&server,len);if(n<0){std::cout<<"connect sock err..."<<std::endl;exit(2);}//连接成功//连接成功后,就可以直接通信了,就可以直接给对方写消息了。
3.连接成功后进行通信
连接成功后,就可以与连接的服务器进行通信了,就可以通过套接字进行数据的传输了。
1.往套接字里写入就使用write,从套接字里读取就使用read。
2.注意使用完该套接字后,就要将套接字关闭。
std::string message;while(true){std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写write(sockfd,message.c_str(),message.size());char outbuffer[1024];//接收服务器发送的加工处理消息int n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}}close(sockfd);
三.version1:单进程版
Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum
{SockError=2,BindError,AcceptError,
};
class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}void START(){lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version1 单进程版本Service(sockfd,clientip,clientport); close(sockfd);//不用了就关闭}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};
Main.cc
#include "tcpserver.hpp"
#include <iostream>
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpserver port
int main(int args,char*argv[])
{if(args!=2){Usage(argv[0]);exit(-1);}//定义一个服务器对象uint16_t port=std::stoi(argv[1]);std::unique_ptr<Tcpserver> tcpsvr(new Tcpserver(port));tcpsvr->Init();tcpsvr->Run();
}
【存在问题】上面属于单进程版本,只有一个进程在执行,只能服务一个客户端,其他客户端要想再使用服务器需要等当前客户端使用完服务器退出才能使用。这也太low了。
所以我们想改进为多进程版本。
单进程就是因为当前进程获取到连接后,该进程就去服务客户端端了,其他客户端就无法再使用该进程了。所以我们可以这样改进:让父进程获取新连接,然后创建子进程,让子进程去提供服务。子进程在服务的过程中,父进程就一直在等待新的连接。
四.version2:多进程版
【处理文件描述符】
多进程版我们首先需要处理一下文件描述符,也就是被打开的套接字。
当获取到新连接后,我们的进程就可以创建子进程,让子进程去跟客户端进行通信。而父进程继续去等待新连接。
因为创建子进程,子进程会继承父进程的文件描述符表。也就是父子进程具有相同的文件描述符表。
1.父进程的主要任务是获取新连接,然后将获取到的套接字给子进程使用。所以它是不需要获取到的套接字的,所以父进程需要将获取到的新套接字关闭掉。
2.而子进程的主要任务是根据新连接服务客户端,它是需要监听套接字的,所以需要将监听套接字关闭掉。父子进程相互关闭文件描述符是不会影响对方的。
【父子进程并发执行】
因为单进程中就是因为进程只能在处于要么在监听中,要么在服务中,不能处于既在监听中,又在服务中。所以我们需要让父进程去获取连接,子进程去服务客户端。而如何做到呢?
父进程是需要等待子进程的,如果子进程不退出,那么它就要一直在阻塞等待。那么它就无法再去获取新连接了。这就跟单进程一样了。
所以我们需要让父进程非阻塞等待子进程。或者通过这样的方法:
让子进程再创建一个子进程,当创建成功时,就直接退出,那么父进程就会等待成功。就会继续往下执行回去新的连接。
而让孙子进程去服务客户端。
Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum
{SockError=2,BindError,AcceptError,
};class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连}void START(){lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version1 单进程版本//Service(sockfd,clientip,clientport); //close(sockfd);//不用了就关闭//-----------version2 多进程版本pid_t id=fork();if(id==0)//子进程,用来处理服务,父进程用来获取新连接{close(_listensock);//子进程不需要该文件关闭if(fork()>0)exit(0);//再创建一个子进程,然后让该进程退出,让孙子进程执行下面的服务,子进程就退出,父进程就等待成功就会重新获取连接Service(sockfd,clientip,clientport);close(sockfd);exit(0);}//父进程只负责用来获取新连接,获取完毕后就交给子进程,自己是不用的,所以关闭close(sockfd);pid_t rid=waitpid(id,nullptr,0);//阻塞等待(void)rid;}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};
五.version3:多线程版
【存在问题】当客户端发起一个连接,服务器获取一个连接时,服务器才忙着创建子进程呢,而创建一个进程的成本是很高的!
所以我们想当在服务器端,获取到一个新连接后,不创建进程而创建线程,让子线程去服务客户端。而主线程继续去获取新的连接。
1.所以只要一旦获取到一个新连接,我们只要当前进程创建线程,将获取的连接的套接字信息以函数参数的形式发送给线程。
2.要让线程去服务连接的客户端,线程不仅需要知道客户端的套接字,还需要知道是要服务谁,也就是客户端的网络信息。
所以我们还需要构建一个线程参数数据结构体,包含线程所需要的客户端的套接字,网络信息等。
【问题1】主线程需要等待子线程吗?
这个问题又回归到刚刚的多进程和单进程中的存在的问题了,如果主线程阻塞等待子线程,那么主线程就无法再重新获取新的连接了。
所以主线程不等待子线程,让子线程自己分离即可。也就是只要获取到一个新连接,那么主线程就创建子线程,创建完后,就不管子线程了,就继续回去获取新的连接。
子线程将自己分离后,就直接去通信即可。
【问题2】在多线程这里需要处理多余的文件描述符吗?
不需要!因为线程所拥有的资源都是共享的!一旦关闭,其他线程就会收到影响。
【问题3】在类内部创建多线程需要将线程函数设置为静态成员函数,而静态成员函数又无法使用通信函数Service,怎么办呢?
所以我们可以在构造线程数据结构体时,将该服务器类对象的指针也构建进去,当创建完线程,线程就可以根据该类指针找到通信函数Service。
在创建线程之前,我们首先将线程所需要的参数数据构建初始化好。然后就可以创建线程,让线程去执行通信函数。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum
{SockError=2,BindError,AcceptError,
};
class Tcpserver;//构建线程所需要的参数数据:获取连接的套接字,客户端端的网络信息,服务器类的指针
class ThreadData
{
public:ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr){}
public: int _sockfd;std::string _ip;uint16_t _port;Tcpserver* _svr;//通过该类指针可以找到类内部函数
};class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象{//子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接pthread_detach(pthread_self());ThreadData* td=static_cast<ThreadData*>(args);//子线程用来服务客户端td->_svr->Service(td->_sockfd,td->_ip,td->_port);delete td;return nullptr;}void Run(){//一启动服务器,就将线程池中的线程创建ThreadPool<TASK>::GetInstance()->Start();//单例对象//静态函数,通过类域就可以使用lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version3 多线程版本ThreadData *td=new ThreadData(sockfd,clientip,clientport,this);//首先将线程所需要的参数数据初始化好pthread_t tid;pthread_create(&tid,nullptr,Routine,td);//Routine要设置成静态成员函数}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};
六.version4:线程池版
【服务器端:构建线程池
【存在问题】虽然该多线程版比多进程版要好些,成本要低一些。但还是存在问题:
1.客户端发起连接,服务器端获取到连接成功后,线程才被创建处理,而线程创建也是需要时间的,所以我们想减少这个时间----我们可以通过线程池来完善。线程池中的线程都是早就被创建好的。只要获取到新连接,就将对应的任务投入到线程池里让线程池里的线程去执行。
2.每来一个用户,就会发起一个连接,就要创建一个线程, 如果客户端不退出,那么线程就一直存在。那么就会存在很多线程的情况。这主要原因是我们的服务器的服务是长时间服务,服务器端是主动退出的,所以客户端不退的话,线程就无法退出。------所以我们想让服务器的服务变成短时间服务,服务完就立马关闭套接字,不再服务。线程就会退出。
所以服务器端只要获取到一个新连接,就将服务客户端的操作让线程池里的线程去执行。
不过线程池里的线程要执行必须是以任务的形式竞争,所以我们需要将服务客户端的动作封装成一个任务,然后将任务投递到线程池里,线程池里线程就会被唤醒去竞争任务。
线程池---被封装成单例模式
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 10;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}void Start(){int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
构建任务,准备投递到线程池里的任务队列里
#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{public: TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port){}void run(){char inbuffer[1024];// while(true)ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client key: "<<inbuffer<<std::endl;//加工处理一下std::string echo_string=init.translation(inbuffer);//将加工处理的数据发送会去n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息if(n<0){//失败了,我们将信号忽略,但将日志打印出来lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));}}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}void operator()(){run();}~TASK(){}
public:int _sockfd;std::string _clientip;uint16_t _clientport;
};
线程池版本
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum
{SockError=2,BindError,AcceptError,
};
class Tcpserver;class ThreadData
{
public:ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr){}
public: int _sockfd;std::string _ip;uint16_t _port;Tcpserver* _svr;
};
class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象{//子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接pthread_detach(pthread_self());ThreadData* td=static_cast<ThreadData*>(args);//子线程用来服务客户端td->_svr->Service(td->_sockfd,td->_ip,td->_port);delete td;return nullptr;}void Run(){//一启动服务器,就将线程池中的线程创建ThreadPool<TASK>::GetInstance()->Start();//单例对象//静态函数,通过类域就可以使用lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//----------version4 线程池版本//当获取到新连接时,就构建任务TASK t(sockfd,clientip,clientport); //将任务放进线程池里,线程就会到线程池里去执行任务。ThreadPool<TASK>::GetInstance()->Push(t);}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};
【服务器端:构建短服务----->翻译字典】
服务器端的服务功能是交给线程池里的线程执行的,所以最好是短服务,如果是长服务的话,那么就会存在很多线程不退出,客户端不主动退出,线程就不退出的场景。我们想要让服务器只服务一次,服务完后,就将对应的服务的套接字关闭,不再服务。那么线程服务完一次就结束了。
所以我们可以设计一个翻译字典的功能,让客户端用户发送字母,服务器端给客户端翻译字母,再将字母的中文发送回去。服务器端服务完一次后,就将监听套接字获取到的新的套接字关闭,不再服务。
如果客户端想要再次请求服务,就需要重新发起连接,让服务器端的监听套接字重新获取到新的套接字。因为原来获取到的套接字已经被关闭。
翻译字典功能
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
const std::string dictname="./dict.txt";
const std::string sep=":";//默认的分割符
extern Log lg;
class Init//相当于加载配置文件的动作,当创建出来,文件内容就已经被加载到map里了
{public:void Spilt(std::string& line,std::string *part1,std::string* part2){auto pos=line.find(sep);if(pos==std::string::npos)return ;*part1=line.substr(0,pos);*part2=line.substr(pos+1);return ;}Init(){std::ifstream in(dictname);//定义一个文件流,成功就打开该文件if(!in.is_open()){lg(Fatal,"ifstream open %s error",strerror(errno));exit(1);} //读取文件里的内容std::string line;//按行读取。读取到line里while(std::getline(in,line)){std::string part1,part2;Spilt(line,&part1,&part2);//然后分割到map里dic.insert({part1,part2});}in.close();}std::string translation(const std::string& key){auto iter=dic.find(key);//返回对应的该值的的迭代器if(iter==dic.end())return "Unkonwn";else return iter->second;}
private:std::unordered_map<std::string,std::string> dic;
};
dict。txt对应的配置文件
apple:苹果
yellow:黄色
red:红色
bule:蓝色
man:男人
woman:女人
hello:你好
让服务器端的翻译字典只服务服务一次,就将对应的套接字关闭:
#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{public: TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port){}void run(){char inbuffer[1024];// while(true)ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client key: "<<inbuffer<<std::endl;//加工处理一下std::string echo_string=init.translation(inbuffer);//将加工处理的数据发送会去n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息if(n<0){//失败了,我们将信号忽略,但将日志打印出来lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));}}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}close(_sockfd);//任务只处理一次,服务器端处理完任务后,就会将该套接字关闭,线程池里的线程就不会一直在执行//客户端如果想再使用服务,需要重新连接,而该套接字已经被关闭,客户端也需要重新创建}void operator()(){run();}~TASK(){}
public:int _sockfd;std::string _clientip;uint16_t _clientport;
};
七.解决细节问题:完善服务器和客户端
1.write写入会存在偶发性失败,进程被信号杀死
write写入功能也是会存在失败的,这种失败是偶发性的,比较小,但也是会发生。比如说,服务器端刚接收到客户端发送的消息,正在处理的过程中,客户端又将连接关闭,那么服务器向往这个套接字里写入数据时,就会写入失败。
而这种失败,就会类似于管道里的,读端关闭,写端再往里写时,系统就会觉得没有用,就会发送信号将该进程杀死。所以这里如果出现了写入失败,也会被操作系统杀死的。而我们不希望服务器端被杀死。因为只是出现了偶发性的写入失败。
所以我们当出现写入失败时,我们应该忽略操作系统发送的信号。防止被杀死。
所以服务器端不仅要对读取做处理,还要对写入做处理。
2.客户端每次请求都需要重新发起连接
因为服务器端,当监听套接字获取到新连接时,就会返回一个新的套接字给线程使用去服务客户端。而线程只服务一次,就会将该套接字关闭。关闭完后,对端的客户端就无zsxxxxxxxxxx法再使用服务了。虽然它对应的连接还在,但是服务器端的连接被关闭了。
所以客户端端再次发送请求时,服务器就接收不到。
【解决方案】
所以客户端在被服务完后,想要再次获取服务,就必须要重新发起连接,这样服务器端才可以获取到对应的连接。并且还需要重新创建套接字,然后再发起连接。因为原来的套接字已经被服务器关闭,不能再使用了。
所以每次客户端想要服务时,都必须重新创建套接字然后发起连接。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{if(args!=3){Usage(argv[0]);exit(1);}std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);while(true){//创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?//连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的connect(sockfd,(struct sockaddr*)&server,len);std::string message;std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写int n=write(sockfd,message.c_str(),message.size());if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接{std::cerr<<"write error..."<<std::endl;continue;}char outbuffer[1024];//接收服务器发送的加工处理消息n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}close(sockfd);}return 0;}
3.服务器端出现问题,客户端需要尝试自救
所以当服务器出现问题时,比如挂掉了。客户端首先要尝试重新连接,看能不能恢复正常。因为服务器端出现问题,那么客户端的写入和读取就可能会出现问题,甚至连接部分会出现问题,这些问题一旦出现,就要尝试重新连接。多次连接,如果服务器又恢复连接了,那么就可以重新连接成功。
否则还是失败,失败了就重连接失败,用户就退出吧不玩了。
1.只是连接部分或者写入读取部分出现错误,客户端创建的套接字并没有问题,所以不需要重新创建。只需要循环连接部分即可。
2.重新连接模块是客户端应该具备的功能。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{if(args!=3){Usage(argv[0]);exit(1);}std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);while(true){//创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?//连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的int cnt=10;bool isreconnect=false;do{ int n=connect(sockfd,(struct sockaddr*)&server,len);if(n<0)//服务器关闭了,肯定会连接失败{isreconnect=true;cnt--;std::cout<<"connect sock err...,cnt: "<<cnt<<std::endl;sleep(12);}else//重连成功了{break;}}while(cnt&&isreconnect);//连接成功//连接成功后,就可以直接通信了,就可以直接给对方写消息了。if(cnt==0){std::cerr<<"user offline.."<<std::endl;break;//用户直接不玩了}std::string message;std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写int n=write(sockfd,message.c_str(),message.size());if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接{std::cerr<<"write error..."<<std::endl;continue;}char outbuffer[1024];//接收服务器发送的加工处理消息n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}close(sockfd);}return 0;}
4.复用ip地址和端口号
将套接字设置成允许复用ip地址和端口号。