【网络编程】套接字编程——TCP通信

文章目录

  • 一、简单的TCP网络程序
    • 1. 单进程版
    • 2. 多进程版
    • 3. 多线程版
  • 二、线程池版TCP网络程序
  • 三、日志与守护进程
    • 1. 日志
    • 2. 守护进程
      • 进程组和会话的引出
      • 守护进程的创建


一、简单的TCP网络程序

1. 单进程版

tcpServer.hpp

#pragma once
#include "err.hpp"
#include 
#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			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(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{

			_quit = false;
			while(!_quit)
			{
				// signal(SIGCHLD, SIG_IGN);

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;
				
				service(sock, clientip, clientport); // 单进程版 
			}
		}
		
		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

【网络编程】套接字编程——TCP通信_第1张图片
【网络编程】套接字编程——TCP通信_第2张图片

因为服务器是单进程版的,所以服务端只有完成一个客户端后才会服务另一个客户端。

【网络编程】套接字编程——TCP通信_第3张图片

主要还是因为当服务端调用service函数进行服务时,在处理一个客户的消息时是一个死循环,只有当一个客户退出后,服务端才会调用accept函数继续获取下一个客户端的连接。

单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此我们需要将该服务器改为多执行流的。


2. 多进程版

当服务端调用accept函数获取新连接后,我们可以创建新的执行流为该连接提供服务,这时候我们可以创建子进程来执行该服务。因此当父进程fork创建出子进程后,父进程就可以继续监听套接字当中获取新连接,而不需要关心获取上来的连接是否已经服务完毕。

文件描述符的继承

我们知道,文件描述符的生命周期是随进程的,子进程创建后会继承父进程的文件描述符表,当父进程打开一个文件时,该文件对应的文件描述符是3, 创建的子进程的3号文件描述符也会指向该文件。如果子进程再创建一个进程,那么该进程的3号文件描述符也会指向和父进程一样的文件。

【网络编程】套接字编程——TCP通信_第4张图片

这里我们需要注意的是,当父进程创建子进程后,父子进程间会保持独立性,此时父进程文件描述符的变化不会影响子进程。套接字文件也是一样,当父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作。进而完成对对应客户端的服务。

父进程等待子进程退出的问题

当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏。因此服务端创建子进程后需要调用wait或者waitpid函数对子进程进行等待。

  • 如果服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完成当前客户端,才能继续获取下一个连接请求,此时服务端仍然是一种串行的方式为客户端提供服务。
  • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有的子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出。

因此服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不是很好的选择,此时我们可以选择让服务端不等待子进程退出。

  1. 捕捉SIGCHLD信号,将其处理动作设置为忽略
  2. 让父进程创建子进程,然后让子进程再创建孙子进程,退出子进程,让孙子进程为客户端提供服务

忽略SIGCHLD信号

void start()
{
	signal(SIGCHLD, SIG_IGN);
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;

		// ————多进程版本实现——忽略信号方式
		pid_t pid = fork();
		if(pid == 0) // 子进程
		{
			close(_listensock);
			// if(fork() > 0) exit(0);
			service(sock, clientip, clientport);
			exit(0);
		}
		else if(pid < 0)
		{
			close(sock);
			continue;
		}
	}
}

演示效果:

【网络编程】套接字编程——TCP通信_第5张图片

创建孙子进程

void start()
{
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;
		

		// ————多进程版本实现——创建孙子进程
		pid_t pid = fork();
		if(pid == 0) // 子进程
		{
			close(_listensock);
			if(fork() > 0) exit(0);
			service(sock, clientip, clientport);
			exit(0);
		}
		else if(pid < 0)
		{
			close(sock);
			continue;
		}

		// 进程等待
		close(sock);
		pid_t ret = waitpid(pid, nullptr, 0);
		if(ret == pid) cout << "wait child " << pid << " success" << std::endl;  
	}
}

演示效果:

【网络编程】套接字编程——TCP通信_第6张图片

由于儿子进程创建完孙子进程后就立刻推出了,因此实际为客户端提供服务的孙子进程就变成了孤儿进程,该进程会被系统领养,当孙子进程为客户端提供完服务退出后系统会回收孙子进程,所以服务进程是不需要等待孙子进程退出的。

关闭文件描述符的重要性

服务进程(爷爷进程)调用accept函数获取到新连接后,会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程,而爸爸进程又会调用fork函数创建出孙子进程,然后再将文件描述符表继承给孙子进程。同样的,对于爸爸进程和孙子进程来说,它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的,因此爸爸进程可以将监听套接字关掉。

  • 对于服务进程来说,当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),如果服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少。
  • 而对于爸爸进程和孙子进程来说,还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作,此时就会对监听套接字当中的数据造成影响。

完整代码: tcpServer.hpp

#pragma once
#include "err.hpp"
#include 
#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			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(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{
			_quit = false;
			while(!_quit)
			{

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;

				// ——多进程版本实现
				pid_t pid = fork();
				if(pid == 0) // 子进程
				{
					close(_listensock);
					if(fork() > 0) exit(0);
					service(sock, clientip, clientport);
					exit(0);
				}
				else if(pid < 0)
				{
					close(sock);
					continue;
				}

				// 进程等待
				close(sock);
				pid_t ret = waitpid(pid, nullptr, 0);
				if(ret == pid) cout << "wait child " << pid << " success" << std::endl;  
			}
		}

		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

3. 多线程版

由于创建进程的成本是比较高的,需要创建该进程对应的进程控制块、进程地址空间、页表等数据结构。而线程的创建要比进程的创建成本会小的多,因为线程会共享进程的大部分资源,因此我们在实现多执行流的服务器时最好采用多线程进行实现。

这里我们需要注意的是,当服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是可以直接访问该文件描述符的。但是此时新线程并不知道他所服务的客户端对应的是哪一个文件描述符,因此我们需要将对应的文件描述符传递给该线程。

但是新线程在为客户端提供服务时就是调用service函数,而调用service函数需要传入三个参数:客户端对应的套接字、IP地址和端口号。而实际在调用pthread_create函数创建新线程时,只能传入类型为void*的参数。所以我们需要设计一个参数结构体ThreadDate,将这三个参数放到该结构体中,当主线程创建新线程时就定义一个ThreadDate对象,将客户端的三个参数设计进该对象中。

// ThreadDate结构体
class ThreadDate
{
public:
	ThreadDate(int fd, const string &ip, const uint16_t &port, TcpServer *ts)
	   :sock(fd), clientip(ip), clientport(port), current(ts)
	{}
public:
	int sock;
	string clientip;
	uint16_t clientport;
	TcpServer* current;
};
void start()
{
	// signal(SIGCHLD, SIG_IGN);
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;

		// V3版本—— 多线程版本
		pthread_t tid;
		ThreadDate* td = new ThreadDate(sock, clientip, clientport, this);
		pthread_create(&tid, nullptr, threadRoutine, td);
	}
}

static void* threadRoutine(void* args)
{
	pthread_detach(pthread_self()); // 线程分离
	ThreadDate* td = static_cast<ThreadDate*>(args);
	td->current->service(td->sock, td->clientip, td->clientport);
	delete td;
	return nullptr;
}

【网络编程】套接字编程——TCP通信_第7张图片

完整代码: tcpServer.hpp

#pragma once
#include "err.hpp"
#include 
#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer;
	class ThreadDate
	{
	public:
		ThreadDate(int fd, const string &ip, const uint16_t &port, TcpServer *ts)
		   :sock(fd), clientip(ip), clientport(port), current(ts)
		{}
	public:
		int sock;
		string clientip;
		uint16_t clientport;
		TcpServer* current;
	};

	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			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(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{
			_quit = false;
			while(!_quit)
			{

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;

				// —— 多线程版本
				pthread_t tid;
				ThreadDate* td = new ThreadDate(sock, clientip, clientport, this);
				pthread_create(&tid, nullptr, threadRoutine, td);
			}
		}

		static void* threadRoutine(void* args)
		{
			pthread_detach(pthread_self()); // 线程分离
			ThreadDate* td = static_cast<ThreadDate*>(args);
			td->current->service(td->sock, td->clientip, td->clientport);
			delete td;
			return nullptr;
		}

		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

tcpServer.cc

#include "tcpServer.hpp"
#include 

using namespace ns_server;

static void usage(string str)
{
	std::cout << "Usage:\n\t" << str << " port\n"
              << std::endl;
}

string echo(const string& message)
{
	return message;
}

int main(int argc, char* argv[])
{
	if(argc != 2)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}

	uint16_t port = atoi(argv[1]);

	unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));

	tsvr->initServer();
	tsvr->start();
	return 0;
}

tcpClient.cc

#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include "err.hpp"
using namespace std;

static void usage(string str)
{
	std::cout << "Usage:\n\t" << str << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}
	
	uint16_t port = atoi(argv[2]);
	string ip = argv[1];

	// 创建socket
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0)
	{
		cerr << "create socket error..." << endl;
		exit(SOCKET_ERR);
	}

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_port = htons(port);
	inet_aton(ip.c_str(), &(server.sin_addr));

	int cnt = 5;
	while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
	{
		sleep(1);
		cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
        if(cnt <= 0) break;
	}

	if(cnt <= 0)
	{
		cerr << "重连失败" << endl;
		exit(CONNECT_ERR);
	}

	// 表示连接服务器成功
	char buffer[1024];
	while(true)
	{
		string line;
		cout << "Enter>>> ";
		getline(cin, line);

		write(sock, line.c_str(), line.size());

		ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
		if(s > 0)
		{
			buffer[s] = 0;
			cout << "server echo >>>" << buffer << endl;
		}
		else if(s == 0)
		{
			cerr << "server quit" << endl;
			break;
		}
		else 
		{
			cerr << "read error:" << strerror(errno) << endl;
			break;
		}
	}

	close(sock);
	return 0;
}

makfile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

二、线程池版TCP网络程序

上面的多线程版服务器有一些问题:

  • 每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样做不仅麻烦,而且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程。
  • 如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也迟迟得不到应答

这里为了解决这个问题我们就需要在服务端引入线程池,线程池的出现解决了处理短时间任务时创建线程与销毁线程的代价,此外,线程池还能保证内核充分利用、防止过度调度。

单例模式线程池的引入:

#pragma once
#include 
#include 
#include 
#include 
#include 
#include "Thread.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"
using namespace std;

const static int N = 5;

// 将此代码设计成单例模式————懒汉模式

template <class T>
class ThreadPool
{
private:
	ThreadPool(int num = N) : _num(num)
	{
		pthread_mutex_init(&_lock, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	ThreadPool(const ThreadPool<T>& tp) = delete;
	void operator=(const ThreadPool<T>& tp) = delete;
public:
	// 设计一个静态成员函数来返回创建的对象
	static ThreadPool<T>* getinstance()
	{
		if(_instance == nullptr)
		{
			LockGuard lockguard(&_instance_lock);
			{
				if(_instance == nullptr)
				{
					cout << "线程池单例形成" << endl;
					_instance = new ThreadPool<T>();
					_instance->init();
					_instance->start();
				}
			}
		}
		return _instance;
	}

	pthread_mutex_t *getlock()
	{
		return &_lock;
	}

	void threadWait()
	{
		pthread_cond_wait(&_cond, &_lock);
	}

	void threadWake()
	{
		pthread_cond_signal(&_cond);
	}

	bool isEmpty()
	{
		return _tasks.empty();
	}

	void init()
	{
		for (int i = 0; i < _num; i++)
		{
			_threads.push_back(Thread(i + 1, threadRoutine, this));
			cout << i + 1 << " thread running" << endl;
		}
	}

	void start()
	{
		for (auto &t : _threads)
		{
			t.run();
		}
	}

	void check()
	{
		for (auto &t : _threads)
			cout << t.threadname() << " running..." << endl;
	}

	static void threadRoutine(void *args)
	{
		ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
		while (true)
		{
			T t;
			// 检测此时有没有任务, 如果有任务就处理任务, 否则就挂起等待
			{
				LockGuard lockguard(tp->getlock());
				while (tp->isEmpty())
				{
					tp->threadWait();
				}
				t = tp->popTask();
			}
			t();
		}
	}

	T popTask()
	{
		T t = _tasks.front();
		_tasks.pop();
		return t;
	}

	void pushTask(const T &t)
	{
		LockGuard lockguard(&_lock);
		_tasks.push(t);
		threadWake();
	}

	~ThreadPool()
	{
		for (auto &t : _threads)
		{
			t.join();
		}
		pthread_mutex_destroy(&_lock);
		pthread_cond_destroy(&_cond);
	}

private:
	vector<Thread> _threads;
	int _num;

	queue<T> _tasks; // 使用stl的自动扩容机制
	pthread_mutex_t _lock;
	pthread_cond_t _cond;

	static ThreadPool<T>* _instance;
	static pthread_mutex_t _instance_lock;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::_instance_lock = PTHREAD_MUTEX_INITIALIZER;

任务类的设计

这里我们还需要设计一个任务类,该任务类中需要包含客户端中的套接字、IP地址和端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个。

为了有利于软件分层,我们可以给任务类中新增一个仿函数成员,当执行任务类中的 重载() 方法时,就可以以回调的方式处理该任务。

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

// using cb_t = function;

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, const string& clientip, const uint16_t& clientport)
	{
		string who = clientip + "-" + to_string(clientport);
		char buffer[1024];
		while(true)
		{
			ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
			if(s > 0)
			{
				buffer[s] = 0;
				cout << who << ">>>" << buffer << endl;
				write(sock, buffer, s);
			}
			else if(s == 0) // 读取到了文件结尾
			{
				// 对方将连接关闭了
				close(sock);
				std::cout << who << " quit, me too" << std::endl;
				break;
			}
			else
			{
				close(sock);
				cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
				break;
			}
		}
	}
};

class Task
{
public:
    Task()
    {}

    Task(int sock, const string& ip, const uint16_t& port)
	:_sock(sock), _ip(ip), _port(port)
    {}

    void operator()()
    {
        _handler(_sock, _ip, _port);
    }

    ~Task()
    {}

private:
    int _sock;
	string _ip;
	uint16_t _port;
	Handler _handler;
};

完整代码:

tcpServer.hpp

#pragma once
#include "err.hpp"
#include 
#include 
#include 
#include 
#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include "ThreadPool.hpp"
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}
			printf("create socket success, code: %d, error string: %s", errno, strerror(errno));

			// 绑定
			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(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}
			printf("bind socket success, code: %d, error string: %s", errno, strerror(errno));

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
			printf("listen socket success, code: %d, error string: %s", errno, strerror(errno));
		}

		void start()
		{

			_quit = false;
			while(!_quit)
			{
				// signal(SIGCHLD, SIG_IGN);

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;
				
				Task t(sock, clientip, clientport, bind(&TcpServer::service, this, placeholders::_1, placeholders::_2, placeholders::_3));
				
				ThreadPool<Task>::getinstance()->pushTask(t);
			}
		}
	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

Thread.hpp

#pragma once

#include 
#include 
#include 
#include 
using namespace std;

class Thread
{
public:
    typedef enum{
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);

public:
    Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args)
    {
        char name[128];
        snprintf(name, 128, "thread-%d", num);
        _name = name;
    }

    int status(){ return _status; }
    string threadname(){ return _name; }

    pthread_t get_id()
    {
        if(_status == RUNNING)
            return _tid;
        else
            return 0;
    }

    static void* thread_run(void* args)
    {
        Thread* ti = static_cast<Thread*>(args);
        (*ti)();
        return nullptr;
    }

    void operator()()
    {
        if(_func != nullptr)
            _func(_args);
    }

    void run() // 封装线程运行
    {
        int n = pthread_create(&_tid, nullptr, thread_run, this);
        if(n != 0)
            exit(-1);
        _status = RUNNING; // 线程状态变为运行
    }

    void join() // 疯转线程等待
    {
        int n = pthread_join(_tid, nullptr);
        if(n != 0)
        {
            cout << "main thread join thread: " << _name << "error" << endl;
            return;
        }
        _status = EXITED;
    }

    ~Thread(){}
private:
    pthread_t _tid;
    string _name;
    func_t _func; // 线程未来要执行的回调
    void* _args;
    ThreadStatus _status;
};

Task.hpp

#pragma once
#include 
#include 
#include 
#include 

using cb_t = std::function<void(int , const std::string &, const uint16_t &)>;

class Task
{
public:
    Task()
    {
    }
    Task(int sock, const std::string &ip, const uint16_t &port, cb_t cb)
    : _sock(sock), _ip(ip), _port(port), _cb(cb)
    {}
    void operator()()
    {
        _cb(_sock, _ip, _port);
    }
    ~Task()
    {
    }

private:
    int _sock;
    std::string _ip;
    uint16_t _port;
    cb_t _cb;
};

【网络编程】套接字编程——TCP通信_第8张图片


三、日志与守护进程

1. 日志

日志(log) 是指在计算机或网络应用中,用于记录系统活动、用户操作、错误信息等的文本或二进制文件。日志文件通常用于故障排除、系统或应用程序性能监控、安全审计等目的。

日志等级

// 枚举日志等级
enum
{
    Debug = 0, // 调试信息
    Info,      // 正常运行
    Warning,   // 报警
    Error,     // 正常错误
    Fatal,     // 严重错误
    Uknown
};

可变参数列表

void logMessage(int level, const char* format, ...)
{
	/*
	    预备
	    va_list p; // char *类型指针
	    int a = va_arg(p, int);  // 根据指定的类型提取参数
	    va_start(p, format); // p指向可变参数部分的起始地址
	    va_end(p); // p = NULL;
	*/
}

打印日志

打印日志时,一般都会有固定的格式,左半部分表示日志的根式,右半部分表示日志的内容。

将可变参数列表元素打印到文件的函数

int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • 参数str:用于缓存格式化字符串结果的字符数组
  • 参数size:限定最多打印到缓冲区sbuf的字符的个数为size-1个,因为vsnprintf还要在结果的末尾追加\0。如果格式化字符串长度大于size-1,则多出的部分被丢弃。如果格式化字符串长度小于等于size-1,则可以格式化的字符串完整打印到缓冲区sbuf。一般这里传递的值就是sbuf缓冲区的长度。
  • 参数format:格式化限定字符串
  • 参数arg:可变长度参数列表
  • 返回:成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。

获取当前时间的函数

time_t time(time_t*tloc);

【网络编程】套接字编程——TCP通信_第9张图片

将时间转换成结构体的函数

struct tm* localtime(const time_t* timep);

【网络编程】套接字编程——TCP通信_第10张图片

返回值:成功就返回struct tm *结构体,失败则返回NULL。

log.hpp

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

const string filename = "log/tcpserver.log";

// 枚举日志等级
enum
{
    Debug = 0, // 调试信息
    Info,      // 正常运行
    Warning,   // 报警
    Error,     // 正常错误
    Fatal,     // 严重错误
    Uknown
};

// 字符串形式获取日志等级
static string toLevelString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Uknown";
    }
}

// 获取日志产生时间
static string getTime()
{
    time_t curr = time(nullptr);
    struct tm* tmp = localtime(&curr);

    // 缓冲区
    char buffer[128];
    snprintf(buffer, sizeof buffer, "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,
            tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    
    return buffer;
}

// 日志格式: 日志等级 时间 pid 消息体 —— 日志函数

void logMessage(int level, const char* format, ...)
{
    char logLeft[1024];
    string level_string = toLevelString(level);
    string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());

    char logRight[1024];
    va_list p;
    va_start(p, format);
	vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);

    // 打印日志
    printf("%s%s\n", logLeft, logRight);

    // 将日志保存到文件中
    FILE* fp = fopen(filename.c_str(), "a");
    if(!fp) return;
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);
    fclose(fp);
}

2. 守护进程

守护进程 是在操作系统中运行的一种特殊类型的后台进程。它的主要任务是监控其他进程的运行状态,并在必要时重新启动它们。守护进程通常在系统启动时启动,并持续运行,直到系统关闭。

守护进程通常被用来确保关键服务或应用程序始终处于运行状态。当一个进程意外终止或崩溃时,守护进程会检测到这种情况,并根据需要采取适当的行动,例如重新启动该进程,以确保系统的稳定性和可用性。

守护进程在操作系统中起到了重要的作用,它可以在后台默默地工作,为其他进程提供支持和保护,并保持系统的稳定运行。

进程组和会话的引出

下面我们来看一些Linux下的一些操作来引出守护进程

  1. 创建一个后台进程 sleep 1000 &,并查看该进程
    在这里插入图片描述
    PPID:父进程的ID。
    PGID:当前进程所属的进程组
    SID:当前进程的 会话 ID。
    TTY:哪一个终端。
  2. 一下创建两个进程,观察现象
    在这里插入图片描述
    这里我们看到这两个进程的PGID是相同的,他们属于同一个进程组,并且以第一个创建的进程作为进程组的组长。
    【网络编程】套接字编程——TCP通信_第11张图片
    bash的PID、PGID和SID都是21894,和先前那两个进程的SID相同。

shell中控制进程组的方式

  1. 查询后台任务jobs
    【网络编程】套接字编程——TCP通信_第12张图片
  2. 将某一任务提到前台运行 fg+任务编号
    【网络编程】套接字编程——TCP通信_第13张图片
  3. ctrl z 让前台的服务暂停,该任务会自动切换到后台
    【网络编程】套接字编程——TCP通信_第14张图片
  4. 让后台暂停的任务重新在后台运行起来 bg+任务编号

结论:

  1. 进程组分为前台任务和后台任务
    【网络编程】套接字编程——TCP通信_第15张图片
  2. 如果将后台任务提到前台,那么老的前台任务就无法运行
    【网络编程】套接字编程——TCP通信_第16张图片
    因此,在一个会话中,只能有一个前台任务在运行。当我们使用ctrl + c将正在运行的前台任务杀死后,bash就会把自己变成前台任务,此时就又可以运行了。

为什么要存在守护进程?

【网络编程】套接字编程——TCP通信_第17张图片

在这里登录就相当于创建一个会话,我们可以在会话中可以启动多个任务,当我们退出会话时,可能会影响会话内部的所有任务。

因此,网络服务器为了不受用户登录注销的影响,通常会以守护进程的方式运行。

【网络编程】套接字编程——TCP通信_第18张图片


守护进程的创建

设置会话的函数 setsid

【网络编程】套接字编程——TCP通信_第19张图片

设置一个会话,以进程组组长ID作为新的会话ID,这里我们需要注意的是调用setsid函数的进程不可以是进程组的组长。

设置成功,则返回调用进程的PID,若设置失败,则返回-1并设置错误码

使用守护进程的条件

  1. 忽略异常
  2. 让当前进程不要成为进程组组长
  3. 新建会话,让自己成为会话的话首进程
  4. 更改守护进程的工作路径
  5. 对0(标准输入)1(标准输出)2(标准错误)做特殊处理

守护进程化的函数 daemon

【网络编程】套接字编程——TCP通信_第20张图片

  • 第一个参数表示是否要更改工作目录,默认不更改
  • 第二个参数表示要不要关闭 0、1、2文件描述符,默认表示不关

err.hpp

#pragma once
enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    SETSID_ERR,
    OPEN_ERR
};

自己实现守护进程 daemon.hpp

// 守护进程

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 


#include "log.hpp"
#include "err.hpp"

// 守护进程的本质是孤儿进程的一种

void Daemon()
{
    // 1. 忽略信号
	signal(SIGPIPE, SIG_IGN);
	signal(SIGCHLD, SIG_IGN);

	// 2. 让当前进程不要成为进程组组长
	if(fork() > 0) exit(0);

	// 3. 新建会话,让自己成为会话的话首进程
	pid_t ret = setsid();
	if((int)ret == -1)
	{
		logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
		exit(SETSID_ERR);
	}

	// 4. 更改守护进程的工作路径 (可不选) —— chdir("/")

	// 5. 处理后续对于0、1、2的问题
	int fd = open("/dev/null", O_RDWR);
	if(fd < 0)
	{
		logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
		exit(OPEN_ERR);
	}
	dup2(fd, 0);
	dup2(fd, 1);
	dup2(fd, 2);
}

【网络编程】套接字编程——TCP通信_第21张图片

【网络编程】套接字编程——TCP通信_第22张图片


你可能感兴趣的:(网络编程,网络,tcp/ip,c++)