前文链接 -- 简单的TCP网络程序·单进程
上篇文章中,实现了TCP网络通信的的单进程版本,因为实现的是一个死循环的逻辑,是串行实运行的,显然这和实际中的TCP通信是不同的,为了解决这方面的问题,需要使用多进程、多线程的方式去解决,这里就直接写了
目录
版本二:多进程
接口1:fork
细节1:父进程直接等待吗?
解决方法1:利用孤儿进程特性
测试结果1
解决方法2:信号捕捉
细节2:父进程要关闭自己不用的文件描述符
测试结果2
多进程版本修改代码
函数1:start()
版本三:多线程
对象类型:pthread_t -- 需填写
接口3:pthread_detach()
接口4:pthread_self()
版本三:多线程版的修改区域
函数2:threadRoutine()
多线程传参对象:ThreadData
细节3:编译的时候记得带上 lpthread
测试3:多线程的测试
细节4:使用本地回环的时候操作符会多用一个
测试4:多线程的测试 -- 在监控脚本下观看链接状态
监控脚本:
全部代码
tcpClient.cc
tcpClient.hpp
tcpServer.cc
tcpServer.hpp
log.hpp
makefile
版本二:多进程
接口1:fork
注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份,也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
细节1:父进程直接等待吗?
显然这里不能单纯的等待,这样又会回归串行运行了,因为等待的时候会阻塞式等待,且这里并不适用非阻塞式等待,因为当有一百个链接来了,就有一百个进程运行,因为非阻塞,就会回到开始,一但后面没有链接到来的话,那么accept这里就等不到了,再也不会返回,这些进程就不会再也不会被回收了
解决方法1:利用孤儿进程特性
我们可以直接让操作系统领养这里的子进程,直接退出,让孙子进程来做服务
测试结果1
解决方法2:信号捕捉
上面的一种用法是一种巧妙的用法,不过我们这里正常来说使用信号捕捉就行了,这样可以避免一直反复在创建子进程
为SIGCHLD建立信号处理程序
即无论一个子进程于何时终止,系统都会向其父进程发送 SIGCHLD 信号**。对该信号的默认处理是将其忽略,不过也可以按照信号处理程序来捕获它
细节2:父进程要关闭自己不用的文件描述符
文件描述符会越来越大,因为老的文件描述符一直正在被使用,如图所示,文件描述符会一直增大,即使链接已经断开了
一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1,这个文件描述符子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了,所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
测试结果2
至此多进程版本完成
多进程版本修改代码
函数1:start()
注意头文件的引入
void start()
{
//解决方法2:信号捕捉
signal(SIGCHLD, SIG_IGN); // SIG_IGN:忽略
for (;;) // 一个死循环
{
// 4. server 获取新链接
// sock 和client 进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
continue;
}
logMessage(NORMAL, "accept a new link success");
std::cout << "sock: " << sock << std::endl;
// 5. 这里就是一个sock, 未来通信我们就用这个sock, 面向字节流的,后续全部都是文件操作!
// 我们就可以直接使用read之类的面向字节流的操作都行
/*
// version 1 -- 单进程
serviceIO(sock);
close(sock); // 走到这里就说明客户端已经关闭
// 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符会越来越少,因为文件描述符本质就是一个数组下标
// 只要是数组下标就会有尽头,提供服务的上限 就等于文件描述符的上限
// 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
*/
// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
pid_t id = fork();
if (id == 0) // 当id为 0 的时候就代表这里是子进程
{
// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
close(_listensock);
//if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
serviceIO(sock);
close(sock);
exit(0);
}
// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
close(sock);
//father
// 那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
// 且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
// 一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了
//不需要等待了
/*pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
std::cout<< "wait success: " << ret << std::endl;
}
*/
}
}
版本三:多线程
由于创建进程的消耗比较大,所以这里再提供一个多线程版本的
对象类型:pthread_t -- 需填写
参考文献 -- pthread_create()函数:创建线程
接口3:pthread_detach()
int pthread_detach(pthread_t thread); 成功:0;失败:错误号
作用:从状态上实现线程分离,注意不是指该线程独自占用地址空间。
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。
接口4:pthread_self()
作用:获取自身id
版本三:多线程版的修改区域
下图有个小错误,应该先close(), 再delete
函数2:threadRoutine()
// 注意因为是静态的所以,不能访问类内成员,所以我们把this指针当成参数传进来
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 线程分离,这样就不需要等待了,自己结束后自己会释放
ThreadData *td = static_cast<ThreadData *>(args);
td->_self->serviceIO(td->_sock);
delete td;
close(td->_sock);
return nullptr;
}
多线程传参对象:ThreadData
在TcpServer类内,所以类内类
// 用以线程传参
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
{}
public:
TcpServer *_self;
int _sock;
};
细节3:编译的时候记得带上 lpthread
测试3:多线程的测试
我们会发现这里的文件操作符会增加,因为随着用户的链接,这里的操作符会被使用,只有当用户断开链接的时候才会被释放
细节4:使用本地回环的时候操作符会多用一个
测试4:多线程的测试 -- 在监控脚本下观看链接状态
监控脚本:
while :; do ps -aL | head -1 && ps -aL | grep tcpserver; sleep 1; echo "#######################"; done
全部代码
tcpClient.cc
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}
// ./tcpclient serverip serverport 调用逻辑
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
tcpClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
TcpClient(const std::string &serverip, const uint16_t &port)
: _sock(1), _serverip(serverip), _serverport(port)
{
}
void initClient()
{
// 1. 创建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 客户端也可以有日志,不过这里就不再实现了,直接打印错误
std::cout << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
// 3. 要不要listen? -- 不需要!客户端不需要建立链接
// 4. 要不要accept? -- 不要!
// 5. 要什么? 要发起链接!
}
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)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串, 截至目前
buffer[n] = 0;
std::cout << "Server回显# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
~TcpClient()
{
if(_sock >= 0) close(_sock); //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
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(port));
tsvr->initServer();
tsvr->start();
return 0;
}
tcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万
class TcpServer; // 声明
// 用以线程传参
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
{}
public:
TcpServer *_self;
int _sock;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 创建socket文件套接字对象 -- 流式套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success");
// 2.bind绑定自己的网路信息 -- 注意包含头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
void start()
{
// 解决方法2:信号捕捉 版本二
// signal(SIGCHLD, SIG_IGN); // SIG_IGN:忽略
for (;;) // 一个死循环
{
// 4. server 获取新链接
// sock 和client 进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
continue;
}
logMessage(NORMAL, "accept a new link success");
std::cout << "sock: " << sock << std::endl;
// 5. 这里就是一个sock, 未来通信我们就用这个sock, 面向字节流的,后续全部都是文件操作!
// 我们就可以直接使用read之类的面向字节流的操作都行
/*
// version 1 -- 单进程
serviceIO(sock);
close(sock); // 走到这里就说明客户端已经关闭
// 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符会越来越少,因为文件描述符本质就是一个数组下标
// 只要是数组下标就会有尽头,提供服务的上限 就等于文件描述符的上限
// 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
*/
// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
/*pid_t id = fork();
if (id == 0) // 当id为 0 的时候就代表这里是子进程
{
// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
close(_listensock);
//if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
serviceIO(sock);
close(sock);
exit(0);
} */
// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
// close(sock);
// father
// 那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
// 且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
// 一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了
// 不需要等待了 version 2
/*pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
std::cout<< "wait success: " << ret << std::endl;
}
*/
// version 3 多线程
// 线程不需要关闭文件操作符,且看的到进程的共享资源
pthread_t tid;
ThreadData *td = new ThreadData(this, sock);
pthread_create(&tid, nullptr, threadRoutine, td); // 把this传过去
// pthread_join(tid, nullptr); 不能阻塞式等待 -- 这样又会会到之前的串行运行了
// 所以我们直接线程分离, 这样就不需要等待了
}
}
// 注意因为是静态的所以,不能访问类内成员,所以我们把this指针当成参数传进来
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 线程分离,这样就不需要等待了,自己结束后自己会释放
ThreadData *td = static_cast<ThreadData *>(args);
td->_self->serviceIO(td->_sock);
close(td->_sock);
delete td;
return nullptr;
}
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()); // 在多路转接的时候再详谈write的返回值
}
else if (n == 0)
{
// 代表client退出 -- 把它想象成一个建立好的管道,客户端不写了,并且把它的文件描述符关了,读端就会像管道一样读到 0 TCP同理
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
~TcpServer() {}
private:
int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
uint16_t _port;
};
} // namespace server
log.hpp
#pragma once
#include <iostream>
#include <string>
// 定义五种不同的信息
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3 //一种不影响服务器的错误
#define FATAL 4 //致命错误
void logMessage(int level, const std::string message)
{
// 格式如下
// [日志等级] [时间戳/时间] [pid] [message]
// [FATAL0] [2023-06-11 16:46:07] [123] [创建套接字失败]
// 暂定
std::cout << message << std::endl;
}
makefile
cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpclient:tcpClient.cc
$(cc) -o $@ $^ -std=c++11
tcpserver:tcpServer.cc
$(cc) -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpserver tcpclient
线程池版本转下篇 -- 简单的TCP网络程序·线程池(后端服务器)