【网络编程】第三章 网络套接字(TCP协议程序+多进程+多线程+线程池)

个人主页:企鹅不叫的博客

专栏

  • C语言初阶和进阶
  • C项目
  • Leetcode刷题
  • 初阶数据结构与算法
  • C++初阶和进阶
  • 《深入理解计算机操作系统》
  • 《高质量C/C++编程》
  • Linux

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

系列文章

【网络编程】第一章 网络基础(协议+OSI+TCPIP+网络传输的流程+IP地址+MAC地址)

【网络编程】第二章 网络套接字(socket+UDP协议程序)


文章目录

  • 系列文章
  • 一、TCP协议程序
    • 1.服务器
      • 创建套接字
      • 绑定端口号
      • listen
      • 监听套接字
      • accept
      • 服务器获取链接
      • 服务器处理业务-回响服务
      • main测试代码
    • 2.客户端
      • 客户端框架和初始化
      • connect
      • 客户端连接
      • 客户端发起请求
      • main测试代码
  • 二、多进程版本
    • 1.捕捉SIGCHLD信号
    • 2.让孙子进程提供服务
  • 三、多线程版本
  • 四、线程池版本


一、TCP协议程序

1.服务器

将TCP服务器封装成一个类,当我们定义出一个服务器对象后需要马上对服务器进行初始化,而初始化TCP服务器要做的第一件事就是创建套接字

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
class TcpServer
{
public:
 TcpServer(int port, string ip)
     :_port(port)
     ,_ip(ip)
     ,_listen_sock(-1)
 {}
	void InitServer(){}
	void start(){}
	~TcpServer()
	{
		if (_listen_sock >= 0){
			close(_listen_sock);
		}
	}
private:
	int _listen_sock; //监听套接字
     int _port;//端口
     string _ip;//ip
};

创建套接字

  • 协议家族选择AF_INET,因为我们要进行的是网络通信。
  • 创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器,SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
  • 协议类型默认设置为0即可
void InitServer()
{
	//创建套接字
        _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sock < 0){
            cout << "socket error" << endl;
            exit(1);
        }
        cout << "socket creat succes, sock: " << _listen_sock << endl;
}

注意:第二个参数和TCP是不同的,填的是SOCK_STREAM

绑定端口号

创建完套接字后我们还需要调用bind函数进行绑定操作,将套接字和网络关联起来

  • 定义一个struct sockaddr_in结构体,将服务器网络相关的属性信息填充到该结构体当中,比如协议家族、IP地址、端口号等。
  • 填充服务器网络相关的属性信息时,协议家族对应就是AF_INET,端口号就是当前TCP服务器程序的端口号。在设置端口号时,需要调用htons函数将端口号由主机序列转为网络序列。
  • 使用的是云服务器,那么在设置服务器的IP地址时,不需要显示绑定IP地址,直接将IP地址设置为INADDR_ANY即可,此时服务器就可以从本地任何一张网卡当中读取数据
  • 填充完服务器网络相关的属性信息后,需要调用bind函数进行绑定
void InitServer()
	{
		//创建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			cout << "socket error" << endl;
			exit(1);
		}
        cout << "socket creat succes, sock: " << _listen_sock << endl;
        //绑定
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
			cout << "bind error" << endl;
			exit(2);
		}
        cout << "bind success" << endl;
	}

memset可以清空结构体,bzero也能清空结构体

void bzero(void *s, size_t n);

listen

#include  
int listen(int sockfd, int backlog); 

作用: 将套接字设置为监听状态,然后去监听socket的到来

参数:

  • sockfd:要设置的套接字(称为监听套接字,通过socket创建)
  • backlog:连接队列的长度,全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可

返回值: 成功返回0,失败返回-1

监听套接字

TCP服务器在创建完套接字和绑定后,需要再进一步将套接字设置为监听状态,监听是否有新的连接到来

	void InitServer()
	{
		//创建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			cout << "socket error" << endl;
			exit(1);
		}
        cout << "socket creat succes, sock: " << _listen_sock << endl;
        //绑定
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
			cout << "bind error" << endl;
			exit(2);
		}
        cout << "bind success" << endl;
        //监听
		if (listen(_listen_sock, 5) < 0){
			cout << "listen error" << endl;
			exit(3);
		}
        cout << "listen success" << endl;
	}

只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成

accept

#include 
#include 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

作用: 接受请求,获取建立好的连接

参数:

  • sockfd:监听套接字
  • addr:输出型参数,获取远端连接的相关信息,包括协议家族、IP地址、端口号
  • addrlen:输入输出型参数,获取addr的大小长度

返回值: 成功返回一个连接套接字,用来标识远端建立好连接的套接字,失败返回-1。

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。

服务器获取链接

通过accept获取连接,一次获取连接失败不要直接将服务端关闭,而是重新去获取连接,调用inet_ntoa函数将整数IP转换成字符串IP,调用ntohs函数将端口号由网络序列转换成主机序列

	void start(){
     //获取连接
     struct sockaddr_in peer;// 获取远端端口号和ip信息
     memset(&peer, '\0', sizeof(peer));
     socklen_t len = sizeof(peer);
     while(1){
         // 获取链接 
         // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
         int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
         if (sock < 0){ 
             cout << "accept fail, continue accept" << endl; 
             continue; 
         } 
         string client_ip = inet_ntoa(peer.sin_addr);
         int client_port = ntohs(peer.sin_port);
         cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<endl;
         //开始服务
         Service(sock, client_ip, client_port);
     }
 }

服务器处理业务-回响服务

accept函数返回的是一个新的套接字,之前的是监听套接字。服务端在为客户端提供服务时就简单的将客户端发来的数据进行输出,并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后再将该数据进行打印输出,此时就能确保服务端和客户端能够正常通信了。

read读取数据

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

write函数写数据

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭,文件描述符本质是数组下标,不用了就释放。在start后定义service函数并实现,并且服务器接收到客户端发送qiut则结束客户端

	void Service(int sock, string client_ip, int client_port){
     char buffer[1024];
     while (1){
         ssize_t size = read(sock, buffer, sizeof(buffer)-1);//默认是字符串
         if (size > 0){ //读取成功
             buffer[size] = '\0';
             if(strcasecmp(buffer, "quit") == 0){	//读到quit就结束
                 cout<<"client quit -- %s[%d]"<< client_ip.c_str() << client_port <<endl;
                 break;
             }
             cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << endl;

             write(sock, buffer, strlen(buffer));
         }
         else if (size == 0){ //对端关闭连接
             cout << client_ip << ":" << client_port << " close!" << endl;
             break;
         }
         else{ //读取失败
             cerr << sock << " read error!" << endl;
             break;
         }
     }
     //服务结束
     close(sock); //归还文件描述符
     cout << client_ip << ":" << client_port << " service done!" << endl;
 }

main测试代码

启动一个服务器格式

//./tcpClient _server_port _server_ip
main (int argc, char* argv[]){ 
 if(argc != 2 && argc != 3){
		cout << "Usage: " << argv[0] << " port" << " IP "<< endl;
		exit(3);
	}
	int port = atoi(argv[1]);
 string ip;
 if(argc == 3) ip = argv[2];
 TcpServer svr(port, ip);
 svr.InitServer();
 svr.start();
 return 0;
}

2.客户端

将客户端封装,客户端初始化只需要创建套接字即可,客户端在调用socket函数创建套接字时,参数设置与服务端创建套接字时是一样的

客户端框架和初始化

class TcpClient
{
public:
	TcpClient(int server_port, string server_ip="")
		: _sock(-1)
		, _server_ip(server_ip)
		, _server_port(server_port)
	{}
	void InitClient()
	{
		//创建套接字
		_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_sock < 0){
			cout << "socket error" << endl;
			exit(2);
		}
     cout << "socket creat succes, sock: " << _sock << endl;
	}
 void start(){}
	~TcpClient()
	{
		if (_sock >= 0){
			close(_sock);
		}
	}
private:
	int _sock; //套接字
	string _server_ip; //服务端IP地址
	int _server_port; //服务端端口号
};

connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

客户端连接

客户端不需要绑定、监听、接收,直接连接就好,Requst函数是发起请求

 void start(){
     struct sockaddr_in peer;
     memset(&peer, '\0', sizeof(peer));

     peer.sin_family = AF_INET;
     peer.sin_port = htons(_server_port);
     peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());

     if (connect(_sock, (struct sockaddr*)&peer, sizeof(peer)) == -1){ //连接失败
         cout << "connect failed..." << endl;
         exit(3);
     }
     else{ //连接成功
         cout << "connect success..." << endl;
         Request(); //发起请求
     }
 }

客户端发起请求

回响服务,当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用write函数向套接字当中写入数据即可

 void Request()
 {
     string msg;
     char buffer[1024];
     while (1){
         cout << "Please Enter# ";
         getline(cin, msg);

         write(_sock, msg.c_str(), msg.size());

         ssize_t size = read(_sock, buffer, sizeof(buffer)-1);
         if (size > 0){//读取成功
             buffer[size] = '\0';
             cout << "server echo# " << buffer << endl;
         }
         else if (size == 0){//读到文件末尾了,客户端可以退出
             cout << "server close!" << endl;
             break;
         }
         else{
             cout << "read error!" << endl;
             break;
         }
     }
 }

main测试代码

客户端程序时我们就需要携带上服务端对应的IP地址和端口号,然后我们就可以通过服务端的IP地址和端口号构造出一个客户端对象,对客户端进行初始后启动客户端

//./tcpClient _server_port _server_ip
int main (int argc, char* argv[]){
 if (argc != 3)
 {
     cout << "Usage: " << argv[0] << " port" << endl;
     return 2;
 }
 string server_ip = argv[2];
	int server_port = atoi(argv[1]);
	TcpClient* clt = new TcpClient(server_port, server_ip);
	clt->InitClient();
	clt->start();
 return 0;
}

结果:客户端输入数据,服务器端处理后会返回给客户端,客户端使用的文件描述符是3,服务器使用的文件描述符是4,当客户端输入qiut时,客户端退出,服务器收到客户端推出的消息

//客户端输入
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8081 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 123
server echo# 123
Please Enter# quit
server close!
----------------------------------------
//服务器接收
[Jungle@VM-20-8-centos:~/lesson38]$ ./server 8081
socket creat succes, sock: 3
bind success
listen success
get a new link->4 [127.0.0.1]:53782
get a new link->4 [127.0.0.1]:53782
client quit -- %s[%d]127.0.0.153782
127.0.0.1:53782 service done!

二、多进程版本

思路:父进程去不断获取连接,获取连接后,让父进程创建一个子进程去为这个获取到的连接提供服务,父子进程是两个不同的执行流,当父进程调用fork创建出子进程后,父进程就可以继续从监听套接字当中获取新连接,而不用关心获取上来的连接是否服务完毕。

子进程继承父进程的文件描述符表:文件描述符表隶属于一个进程,所以当父进程打开文件描述符3,子进程打开的也是文件描述符3。父子进程之间具有独立性。对于套接字也是一样,父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作

父进程等待子进程:如果父进程等待子进程的话,父进程就需要阻塞,无法去获取到新的连接,如果不等待的话,子进程退出,子进程的资源就没有人回收,就变成僵尸进程,如果采用非阻塞等待,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出

解决方法:

让父进程不等待子进程退出,常见的方式有两种:

  • 捕捉SIGCHLD信号,将其处理动作设置为忽略。
  • 让父进程创建子进程,子进程再创建孙子进程,最后让孙子进程为客户端提供服务。

监控进程脚本:

while :; do ps axj | head -1 && ps axj | grep server | grep -v grep;echo "######################";sleep 1;done

1.捕捉SIGCHLD信号

当子进程退出时会给父进程发送SIGCHLD信号,如果父进程将SIGCHLD信号进行捕捉,并将该信号的处理动作设置为忽略,此时父进程就只需专心处理自己的工作,不必关心子进程了。

	void start(){
     //获取连接
     struct sockaddr_in peer;// 获取远端端口号和ip信息
     memset(&peer, '\0', sizeof(peer));
     socklen_t len = sizeof(peer);
     while(1){
         // 获取链接 
         // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
         int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
         if (sock < 0){ 
             cout << "accept fail, continue accept" << endl; 
             continue; 
         } 
         string client_ip = inet_ntoa(peer.sin_addr);
         int client_port = ntohs(peer.sin_port);
         cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<endl;
         // //开始服务
         pid_t id = fork();
         if (id == 0){ //child
             //处理请求
             Service(sock, client_ip, client_port);
             exit(0); //子进程提供完服务退出
         }
     }
 }

结果:服务器中的主线程占用了文件描述符3,客户端进程1占用文件描述符4,客户端进程2占用文件描述符5

//服务器
[Jungle@VM-20-8-centos:~/lesson38]$ ./server 8080
socket creat succes, sock: 3
bind success
listen success
get a new link->4 [127.0.0.1]:57544
get a new link->5 [127.0.0.1]:57556
get a new link->4 [127.0.0.1]:57544
get a new link->5 [127.0.0.1]:57556
-------------------------------------------------
//客户端1
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8080 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 1
server echo# 1
//客户端2
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8080 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 2
server echo# 2

2.让孙子进程提供服务

服务端创建出来的子进程再次进行fork,让孙子进程为客户端提供服务, 此时我们就不用等待孙子进程退出了

命名说明:

  • 爷爷进程:在服务端调用accept函数获取客户端连接请求的进程。
  • 爸爸进程:由爷爷进程调用fork函数创建出来的进程。
  • 孙子进程:由爸爸进程调用fork函数创建出来的进程,该进程调用Service函数为客户端提供服务。

让爸爸进程创建完孙子进程后立刻退出,此时爷爷进程可以立刻回收爸爸进程,然后继续accept其他客户端连接请求。

爸爸进程退出后,孙子进程就会变成孤儿进程,由系统接管,孙子进程完成服务后会自动退出,所以爷爷进程是不需要等待孙子进程的。

注意:爷爷进程创建爸爸进程后,爸爸进程会继承爷爷进程的文件文件描述符,爸爸进程创建孙子进程后,孙子进程会继承爸爸进程的文件描述符,对于父子进城来说,文件描述符表是互相独立的,同时爸爸进程和孙子进程是不关心从爷爷进程继承下来的监听套接字,所以爸爸进程直接关掉监听套接字。

必要性:系统调用过程中,是会不断占用套接字数量,同时对于爸爸进程和孙子进程来说,如果不及时关闭监听套接字,会造成套接字泄漏

void start(){
    //获取连接
    struct sockaddr_in peer;// 获取远端端口号和ip信息
    memset(&peer, '\0', sizeof(peer));
    socklen_t len = sizeof(peer);
    while(1){
        // 获取链接 
        // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
        int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
        if (sock < 0){ 
            cout << "accept fail, continue accept" << endl; 
            continue; 
        } 
        string client_ip = inet_ntoa(peer.sin_addr);
        int client_port = ntohs(peer.sin_port);
        cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<endl;
        // //开始服务
        pid_t id = fork();
        if (id == 0){ //child
            // 子进程
            // 父子进程的文件描述符内容一致
            // 子进程可以关闭监听套接字的文件描述符
            close(_listen_sock); // 可以不关闭,但是建议关闭,以防后期子进程对监听套接字fd进行了一些操作,对父进程造成影响
            if (fork() > 0){
                exit(0); //爸爸进程直接退出
            }
            //孙子进程
            //处理请求
            Service(sock, client_ip, client_port); //孙子进程提供服务
            exit(0); //孙子进程提供完服务退出
        }
        close(sock); //father关闭为连接提供服务的套接字,不关闭的话,套接字会越来越少
        // 爷爷进程等爸爸进程
        waitpid(id, nullptr, 0); // 以阻塞方式等待,但这里不会阻塞,因为爸爸进程是立即退出的
    }
	}

结果:服务器启动后,由于我们及时删除不必要的套接字,所以除了服务器使用的文件描述符是3,其他客户端服务器使用的文件描述符是4

//服务器启动
[Jungle@VM-20-8-centos:~/lesson38]$ ./server 8081
socket creat succes, sock: 3
bind success
listen success
get a new link->4 [127.0.0.1]:57552
get a new link->4 [127.0.0.1]:57584
get a new link->4 [127.0.0.1]:57584
get a new link->4 [127.0.0.1]:57552
-----------------------------------------------
//客户端1接入服务器
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8081 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 2
server echo# 2
-----------------------------------------------
//客户端1接入服务器
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8081 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 1
server echo# 1

三、多线程版本

思路:当服务进程调用accept函数获取到一个新连接后,创建一个线程为客户端提供服务,创建好的线程之间进行线程分离(线程也是需要退出的,否则会造成类似进程的僵尸问题),这样主线程就不需要等待其它线程了。

各个线程共享同一个文件描述符:所以当主线程accept到一个文件描述符时,其他线程可以直接访问到这个文件描述符的。

虽然次线程可以直接访问文件描述符,但是次线程不知道它所服务的客户端对应的时哪个文件描述符,因此主线程创建次线程后需要告诉新线程对应应该访问的文件描述符的值

线程数据类:调用Service函数时,需要传入客户端对应的套接字、IP地址和端口号,但是由于主线程创建新线程时只能传入一个参数,所以将客户端对应的套接字、IP地址和端口号设计到ThreadData类当中,然后将ThreadData类对象的地址,作为新线程的参数传入

class ThreadData
{
public:
 int _port;
 string _ip;
 int _sock;

 ThreadData(int port, string ip, int sock,  TcpServer *ts)
     : _port(port)
		, _ip(ip)
		, _sock(sock)
 {}
};

文件描述符关闭:所有线程共享一个文件描述符

  • 对于主线程accept上来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。
  • 对于监听套接字,虽然创建出来的新线程不必关心监听套接字,但新线程不能将监听套接字对应的文件描述符关闭,否则主线程就无法从监听套接字当中获取新连接了。

Service函数定义为静态成员函数:调用pthread_create函数创建线程时,新线程的执行例程是一个参数为void*,返回值为void*的函数。如果我们要将这个执行例程定义到类内,就需要将其定义为静态成员函数,否则这个执行例程的第一个参数是隐藏的this指针。

由于执行例程是静态成员函数,静态成员函数无法调用非静态成员函数,因此我们需要将Service函数定义为静态成员函数,直接在Service函数前面加上一个static即可。

	static void* threadRoutine(void* arg){
     pthread_detach(pthread_self()); //分离线程
     ThreadData* td = (ThreadData*)arg;
     Service(td->_sock, td->_ip, td->_port); //线程为客户端提供服务
     delete td; //释放参数占用的堆空间
     return nullptr;
 }
	void start(){
     //获取连接
     struct sockaddr_in peer;// 获取远端端口号和ip信息
     memset(&peer, '\0', sizeof(peer));
     socklen_t len = sizeof(peer);
     while(1){
         // 获取链接 
         // sock 是进行通信的一个套接字  _listen_sock 是进行监听获取链接的一个套接字 
         int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
         if (sock < 0){ 
             cout << "accept fail, continue accept" << endl; 
             continue; 
         } 
         string client_ip = inet_ntoa(peer.sin_addr);
         int client_port = ntohs(peer.sin_port);
         cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<endl;
         // //开始服务
         pthread_t tid;
         ThreadData* td = new ThreadData(client_port, client_ip, sock);
         pthread_create(&tid, nullptr, threadRoutine, (void*)td);
     }
 }
static void Service(int sock, string client_ip, int client_port){}

监控线程脚本:

while :; do ps -aL|head -1&&ps -aL|grep server;echo "####################";sleep 1;done

结果:无论有多少个客户端发来连接请求,在服务端都会创建出相应数量的新线程为对应客户端提供服务,而当客户端一个个退出后,为其提供服务的新线程也就会相继退出,最终就只剩下最初的主线程仍在等待新连接的到来

//服务器
[Jungle@VM-20-8-centos:~/lesson38]$ ./server 8080
socket creat succes, sock: 3
bind success
listen success
get a new link->4 [127.0.0.1]:60968
get a new link->5 [127.0.0.1]:60986
get a new link->5 [127.0.0.1]:60986
get a new link->4 [127.0.0.1]:60968
//客户端1
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8080 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 2
server echo# 2
//客户端2
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8080 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 1
server echo# 1

四、线程池版本

来一个连接就创建一个线程,断开一个连接就释放一个线程,这样频繁地创建和释放线程资源,对OS来说是一种负担,同时也带来资源的浪费,所以将每一个客户端都封装程一个类,让线程池处理。为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价,此外,线程池还能够保证内核充分利用,防止过分调度

线程池详解

线程池代码如下:在头文件threadpool.hpp中

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "task.hpp"

using namespace std;

#define NUM 5

template <class T>
class ThreadPool
{
public:
 ThreadPool(int threadNum = NUM) 
     :threadNum_(threadNum)
     ,isStart_(false)
 {
     pthread_mutex_init(&mutex_, nullptr);
     pthread_cond_init(&cond_, nullptr);
 }
 ~ThreadPool()
 {
     pthread_mutex_destroy(&mutex_);
     pthread_cond_destroy(&cond_);
 }
 //成员函数,都有默认参数this,而static函数没有this指针
 static void *threadRoutine(void *args)
 {
     pthread_detach(pthread_self());
     ThreadPool<T> *tp = (ThreadPool<T> *)args;
     while (1)
     {
         tp->lockQueue();
         while (tp->IsEmpty())
         {
             tp->Wait();
         }
         //拿任务
         T t = tp->pop();
         tp->unlockQueue();
         // 解锁后处理任务
         t.Run();
     }
 }
 void start()
 {
     assert(!isStart_);
     for (int i = 0; i < threadNum_; i++)
     {
         pthread_t temp;
         pthread_create(&temp, nullptr, threadRoutine, this);//注意参数传入this指针
     }
     isStart_ = true;
 }

 //往任务队列塞任务(主线程调用)
 void push(const T &task)
 {
     lockQueue();
     taskQueue_.push(task);
     WakeUp();
     unlockQueue();
 }
 //从任务队列获取任务(线程池中的线程调用)
 T pop()
 {
     T temp = taskQueue_.front();
     taskQueue_.pop();
     return temp;
 }
 //加锁
 void lockQueue() 
 { 
     pthread_mutex_lock(&mutex_); 
 }
 //解锁
 void unlockQueue() 
 { 
     pthread_mutex_unlock(&mutex_); 
 }
 //判空
 bool IsEmpty()
 {
     return taskQueue_.empty(); 
 }
 //等待
 void Wait() 
 { 
     pthread_cond_wait(&cond_, &mutex_);
 }
 //唤醒
 void WakeUp() 
 { 
     pthread_cond_signal(&cond_); 
 }
private:
 bool isStart_;          //线程池是否开始
 int threadNum_;         //线程池中线程的数量
 queue<T> taskQueue_;    //任务队列
 pthread_mutex_t mutex_;
 pthread_cond_t cond_;
};

由于引入了线程池,所以服务器端需要加入一个指向线程池的指针设置为空,实例化服务器对象时,先将这个线程池指针先初始化为空,然后初始化线程池中的五个对象,然后服务器用aceept函数接收一个请求后,会根据客户端的套接字、IP地址以及端口号构建出一个任务创建一个任务,然后调用线程池的接口,将任务push到线程池当中。

class TcpServer
{
public:
 TcpServer(int port, string ip)
     :_port(port)
     ,_ip(ip)
     ,_listen_sock(-1)
     , _tp(nullptr)
 {}
	void InitServer()
	{
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			cout << "socket error" << endl;
			exit(1);
		}
     cout << "socket creat succes, sock: " << _listen_sock << endl;
		struct sockaddr_in local;
		memset(&local, '\0', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) == -1){
			cout << "bind error" << endl;
			exit(2);
		}
     cout << "bind success" << endl;
		if (listen(_listen_sock, 5) < 0){
			cout << "listen error" << endl;
			exit(3);
		}
     cout << "listen success" << endl;
		//构造线程池对象
		_tp = new ThreadPool<Task>(); 
	}
	void start(){
		//初始化线程池
		_tp->start(); 
		struct sockaddr_in peer;
		memset(&peer, '\0', sizeof(peer));
		socklen_t len = sizeof(peer);
		while(1){
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){ 
				cout << "accept fail, continue accept" << endl; 
				continue; 
			} 
			string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			cout<<"get a new link->"<<sock<<" ["<<client_ip<<"]:"<<client_port<<endl;
			// //开始服务
			Task task(sock, client_ip, client_port); //构造任务
			_tp->push(task); //将任务Push进任务队列
		}
	}

	static void Service(int sock, string client_ip, int client_port){
		char buffer[1024];
		while (1){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);//默认是字符串
			if (size > 0){ //读取成功
				buffer[size] = '\0';
				if(strcasecmp(buffer, "quit") == 0){	//读到quit就结束
					cout<<"client quit -- %s[%d]"<< client_ip.c_str() << client_port <<endl;
					break;
				}
				cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << endl;

				write(sock, buffer, strlen(buffer));
			}
			else if (size == 0){ //对端关闭连接
				cout << client_ip << ":" << client_port << " close!" << endl;
				break;
			}
			else{ //读取失败
				cerr << sock << " read error!" << endl;
				break;
			}
		}
		close(sock); 
		cout << client_ip << ":" << client_port << " service done!" << endl;
	}
private:
	int _listen_sock; //监听套接字
	int _port;//端口
	string _ip;//ip
	ThreadPool<Task>* _tp; //线程池
};

设计任务类,该任务类当中需要包含客户端对应的套接字、IP地址、端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个,同时线程池中的线程会通过Run方法对任务处理,实际处理的方法就是服务器中的service,放在task.hpp这个头文件下

#pragma once
#include 
#include 
#include 
using namespace std;

static void Service(int sock, string ip, int port)
{
while (1){
 char buf[1024];
 ssize_t size = read(sock, buf, sizeof(buf)-1);
 if (size > 0){
   // 正常读取size字节的数据
   buf[size] = 0;
   cout << "[" << ip << "]:[" << port  << "]# "<< buf << std::endl;
   string msg = "server get!-> ";
   msg += buf;
   write(sock, msg.c_str(), msg.size());
 }
 else if (size == 0){
   // 对端关闭
   cout << "[" << ip << "]:[" << port  << "]# close" << endl;
   break;
 }
 else{
   // 出错
   cerr << sock << "read error" << endl; 
   break;
 }
}

close(sock);
cout << "service done" << endl;
}

struct Task
{
int _port;
string _ip;
int _sock;

Task(int sock, string ip, int port)
 :_port(port)
 ,_ip(ip)
 ,_sock(sock)
{}
void Run()
{
 Service(_sock, _ip, _port);
}
};

监控脚本:

while :; do ps -aL|head -1&&ps -aL|grep server;echo "####################";sleep 1;done

结果:无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出

//服务器
[Jungle@VM-20-8-centos:~/lesson38]$ ./server 8080
socket creat succes, sock: 3
bind success
listen success
get a new link->4 [127.0.0.1]:46612
[127.0.0.1]:[46612]# 123
--------------------------------------------
//客户端
[Jungle@VM-20-8-centos:~/lesson38]$ ./client 8080 127.0.0.1
socket creat succes, sock: 3
connect success...
Please Enter# 123
server echo# server get!-> 123

你可能感兴趣的:(计算机网络,网络,tcp/ip,网络协议)