目录
1.简单的TCP网络程序
1.1.读取信息函数read函数和发送信息函数write函数
1.2.简单的TCP网络程序(单进程版)
1.3.简单的TCP网络程序(多进程版)
1.4.简单的TCP网络程序(多线程版)
1.5.简单的TCP网络程序(线程池版)
read函数:
fd:特定的文件描述符,表示从该文件描述符中读取数据。
buf:数据的存储位置,表示将读取到的数据存储到该位置。
count:数据的个数,表示从该文件描述符中读取数据的字节数。
返回值:如果返回值大于0,则表示本次实际读取到的字节个数。
如果返回值等于0,则表示对端已经把连接关闭了。
如果返回值小于0,则表示读取时遇到了错误。
注:1.read返回值为0表示对端连接关闭。
这实际和本地进程间通信中的管道通信是类似的,当使用管道进行通信时,可能会出现如下情况:(1)写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
(2)读端进程不读,写端进程一直写,此时当管道被写满后写端进程就会被挂起,因为此时空间没有就绪。
(3)写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
(4)读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。
这里的写端就对应客户端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。2.使用read函数需要包含
头文件。
write函数:
write函数可以向套接字中写入数据。
参数:fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
buf:需要写入的数据。
count:需要写入数据的字节个数。
返回值:写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。
注:1.使用write函数需要包含
头文件。
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建Makefile文件,写入下图五所示的代码,使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用netstat -lntp命令作为监控脚本,如下图六所示。
netstat -lntp命令可以看到我们服务器程序tcpServer的网络状态,Proto是服务类型这里为tcp的,Recv-Q是收到的消息数,这里为0,Send-Q是发送的消息数,这里为0,Local是本地绑定的IP地址,这里为0.0.0.0,即全地址绑定,Address是绑定的端口号,这里是8081,Foreign为允许访问的远端主机IP,这里为全部主机,Address为允许访问的远端主机的端口,这里为全部主机全部端口,State为当前套接字状态,这里为LISTEN监听状态。
serverTcp.cc文件:
#include "util.hpp" #include
#include #include #include class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. bind绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 本地socket信息,写入sock_对应的内核区域 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3. 监听socket,为何要监听呢?tcp是面向连接的! if (listen(listenSock_, 5 /*后面再说*/) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 运行别人来连接你了 } void loop() { while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4. 获取连接, accept 的返回值是一个新的socket fd ?? int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5 提供服务, echo -> 小写 -> 大写 // 5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行, //只能提供完毕服务之后才能进行accept transService(serviceSock, peerIp, peerPort); } } // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if(strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 可以进行大小写转化了 for(int i = 0; i < s; i++) { if(isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } // 只要走到这里,一定是client退出了,服务到此结束 close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏! logMessage(DEBUG, "server close %d done", sock); } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8081 127.0.0.1\n" << std::endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if(argc != 2 && argc != 3 ) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if(argc == 3) ip = argv[2]; ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; } clientTcp.cc文件:
#include "util.hpp" // 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!! // 3. 需要listen吗?不需要的! // 4. 需要accept吗?不需要的! volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1. 创建socket SOCK_STREAM int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,connect 会自动帮我们进行bind! if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << sock << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(sock); return 0; }
log.hpp文件:
#pragma once #include
#include #include #include #include #include #include #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr:stdout; fprintf(out, "%s | %u | %s | %s\n", \ log_level[level], \ (unsigned int)time(nullptr),\ name == nullptr ? "unknow":name,\ logInfo); } util.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #define SOCKET_ERR 1 #define BIND_ERR 2 #define LISTEN_ERR 3 #define USAGE_ERR 4 #define CONN_ERR 5 #define BUFFER_SIZE 1024 Makefile文件:
.PHONY:all all:clientTcp serverTcp clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcp:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcp clientTcp
注:
1.在UDP部分我们使用inet_addr函数将字符串风格的点分十进制IP地址转换成整型的四字节IP地址,我们也可以使用inet_aton函数来进行转换,inet_aton函数声明如下图所示,参数cp为要转换的字符串风格的点分十进制IP地址,参数inp为一个输出型参数,将转换后的整型的四字节IP地址赋值给inp指向变量。
使用inet_aton函数需要包含
头文件。 2.UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。
因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态,设置监听状态需要使用listen函数,listen函数声明如下图所示。listen函数是将对应套接字从普通套接字转为监听套接字。
在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成。
参数:
sockfd:需要设置为监听状态的套接字对应的文件描述符。
backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
返回值:监听成功返回0,监听失败返回-1,同时错误码会被设置。
使用listen函数需要包含
头文件。 3.TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先使用accept函数获取到客户端的连接请求。accept函数声明如下图所示。
accept函数的功能是获取一个连接。
参数:
sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
addr:输出型参数,对端网络相关的属性信息,包括协议家族、IP地址、端口号等。与recvfrom函数的src_addr参数用法相同。
addrlen:输入输出型参数,调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度。与recvfrom函数的addrlen参数用法相同。
返回值:获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。
使用accept函数需要包含
头文件。 4.accept函数返回值问题。
问题:accept函数返回的套接字是什么?
答:调用accept函数获取连接时,是从监听套接字当中获取的。如果accept函数获取连接成功,此时会返回接收到的套接字对应的文件描述符。
监听套接字与accept函数返回的套接字的作用:
· 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
· accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。5.服务端获取连接的详细介绍。
accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取连接。
使用./tcp_server 8081命令运行tcp服务器后,即使还没有编写客户端相关的代码,但是我们可以使用telnet 127.0.0.1 8081命令远程登录到该服务器,因为telnet底层实际采用的就是TCP协议,也可以打开浏览器在网页框中输入116.205.239.168:8081(假设云服务器主机IP地址为116.205.239.168),因为网页访问实际也采用的就是TCP协议(网页访问采用多线程,可能一次访问发起多次请求),如下图所示。
使用telnet命令或网页访问连接当前TCP服务器后可以看到,此时服务器接收到了一个连接,为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的,其分别对应标准输入流、标准输出流和标准错误流,而3号文件描述符在初始化服务器时分配给了监听套接字,因此当第一个客户端发起连接请求时,为该客户端提供服务的套接字对应的文件描述符就是4。
6.服务器accept获取连接请求后,下面当然就是要对获取到的连接进行处理。但此时为客户端提供服务的不是监听套接字,因为监听套接字获取到一个连接后会继续获取下一个请求连接,为对应客户端提供服务的套接字实际是accept函数返回的套接字,下面就将其称为“服务套接字”。
TCP是流式套接字,面对的是字节流,因此TCP服务器从服务套接字中读数据和写数据使用的是read接口和write接口。这里使用read函数读取数据后使用strcasecmp函数进行比较,如果客户端输入了quit那么服务端就不用为该客户提供服务了,如果客户端没有输入quit,那么就提供大小写转化服务。strcasecmp函数声明如下图所示,其功能是忽略大小写的比较。
使用strcasecmp函数需要包含
头文件。 7.在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符使用close函数关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。
8.在TCP服务端,读取数据是服务套接字中读取的,而写入数据的时候也是写入进服务套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。
前面学习的UDP服务端,读数据和写数据也是在一个套接字中进行的,因此UDP服务端也是全双工的。
9.客户端不需要进行绑定和监听:
· 服务端要进行绑定是因为服务端的IP地址和端口号必须要众所周知,不能随意改变。而客户端虽然也需要IP地址和端口号,但是客户端并不需要我们进行绑定操作,客户端连接服务端时系统会自动指定一个端口号给客户端。
· 服务端需要进行监听是因为服务端需要通过监听来获取新连接,但是不会有人主动连接客户端,因此客户端是不需要进行监听操作的。
此外,客户端必须要知道它要连接的服务端的IP地址和端口号,因此客户端除了要有自己的套接字之外,还需要知道服务端的IP地址和端口号,这样客户端才能够通过套接字向指定服务器进行通信。10.由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以使用connect函数向服务端发起连接请求。connect函数声明如下图所示。
参数:
sockfd:特定的套接字,表示通过该套接字发起连接请求。
addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。与sendto函数的dest_addr参数用法相同。
addrlen:传入的addr结构体的长度。与sendto函数的addrlen参数用法相同。
返回值:
连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
使用connect函数需要包含
头文件。 11.使用./serverTcp命令运行服务端后,该版本的服务端是单进程的调用transService函数提供服务的,而transService函数内部是死循环提供服务的,一旦调用了transService函数,该函数就不会退出,直到对应客户端退出为止,客户端退出了服务端才会再次调用transService函数给其他客户端提供服务,如下图所示。
问题:后连接的客户端为什么开始会显示连接成功?
答:当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了。实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建Makefile文件,写入下图五所示的代码,使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,如下图六所示。
serverTcp.cc文件:
#include "util.hpp" #include
#include #include #include class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. bind绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 本地socket信息,写入sock_对应的内核区域 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3. 监听socket,为何要监听呢?tcp是面向连接的! if (listen(listenSock_, 5 /*后面再说*/) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 运行别人来连接你了 } void loop() { while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4. 获取连接, accept 的返回值是一个新的socket fd ?? int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5 提供服务, echo -> 小写 -> 大写 // 5.1 v1.1 版本 -- 多进程版本 -- 也是可以的 // 爷爷进程 pid_t id = fork(); if(id == 0) { // 爸爸进程 close(listenSock_);//建议 // 又进行了一次fork,fork成功,创建出孙子进程后直接退出爸爸进程 if(fork() > 0) exit(0); // 孙子进程 -- 没有爸爸 -- 孤儿进程 -- 被系统领养 -- 回收问题就交给了系统来回收 transService(serviceSock, peerIp, peerPort); exit(0); } // 爷爷进程 close(serviceSock); //这一步是一定要做的! // 爸爸进程直接终止,爷爷进程等待时立马得到退出码,释放僵尸进程状态 pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式 assert(ret > 0); (void)ret; } } // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if(strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 可以进行大小写转化了 for(int i = 0; i < s; i++) { if(isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } // 只要走到这里,一定是client退出了,服务到此结束 close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏! logMessage(DEBUG, "server close %d done", sock); } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8081 127.0.0.1\n" << std::endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if(argc != 2 && argc != 3 ) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if(argc == 3) ip = argv[2]; ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; } clientTcp.cc文件:
#include "util.hpp" // 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!! // 3. 需要listen吗?不需要的! // 4. 需要accept吗?不需要的! volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1. 创建socket SOCK_STREAM int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,connect 会自动帮我们进行bind! if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << sock << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(sock); return 0; }
log.hpp文件:
#pragma once #include
#include #include #include #include #include #include #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr:stdout; fprintf(out, "%s | %u | %s | %s\n", \ log_level[level], \ (unsigned int)time(nullptr),\ name == nullptr ? "unknow":name,\ logInfo); } util.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #define SOCKET_ERR 1 #define BIND_ERR 2 #define LISTEN_ERR 3 #define USAGE_ERR 4 #define CONN_ERR 5 #define BUFFER_SIZE 1024 Makefile文件:
.PHONY:all all:clientTcp serverTcp clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcp:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcp clientTcp
注:
1.每有一个客户端进行连接,父进程就创建一个子进程来对对应客户端提供服务。每有一个客户端进行连接,父进程就会重新创建一个套接字(文件描述符)serviceSock并更新peerIp和peerPort,后面创建对应子进程时就会继承此刻父进程的这些信息,去给对应客户端提供服务。
父进程每一次与一个客户端建立连接,创建一个子进程给该客户端进行服务后,因为子进程已经继承了父进程中对应客户端的所有信息,而父子进程是两个不同的执行流,父进程此时可以关闭本执行流下的服务套接字了。由于父子进程是两个不同的执行流,每一个子进程都会继承父进程的监听套接字,因为子进程不需要使用监听套接字,所以建议子进程执行前先将监听套接字关闭。
父进程如果阻塞式的等待子进程,那就和单进程TCP服务器没有区别了,如果进行非阻塞等待是可以的,但是我们需要创建一个vector将所有的子进程pid保存起来,然后轮询式的非阻塞等待每一个子进程,比较麻烦。(如果不等待会有僵尸进程的问题)
对于父进程等待的问题,有两种方案:
方案一(仅Linux下可使用):子进程在退出的时候会向父进程发送SIGCHLD信号,父进程可以使用signal函数重定义SIGCHLD信号的处理方法,让其直接退出即可。loop函数部分的代码如下图所示。该方案需要使用signal函数,因此该方案仅在Linux下可使用。
方案二(都可使用):主进程每次创建子进程时,先创建子进程然后让子进程创建孙子进程,然后关闭子进程,这样孙子进程就成为孤儿进程了,会被操作系统领养,孙子进程的回收问题就交给了操作系统。孙子进程去执行transService服务,执行完后被操作系统回收。主进程在waitpid回收等待子进程时,由于子进程会被直接终止,所以会立马得到退出码释放僵尸状态。oop函数部分的代码如下图所示。
2.创建子进程的成本较高,我们有没有成本低的方法呢?有的,使用多线程。
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建Makefile文件,写入下图五所示的代码,使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,如下图六所示。
serverTcp.cc文件:
#include "util.hpp" #include
#include #include #include class ServerTcp; // 申明一下ServerTcp class ThreadData { public: uint16_t clientPort_; std::string clinetIp_; int sock_; ServerTcp *this_; public: ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts) : clientPort_(port), clinetIp_(ip), sock_(sock),this_(ts) {} }; class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. bind绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 本地socket信息,写入sock_对应的内核区域 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3. 监听socket,为何要监听呢?tcp是面向连接的! if (listen(listenSock_, 5 /*后面再说*/) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 运行别人来连接你了 } static void *threadRoutine(void *args) { pthread_detach(pthread_self()); //设置线程分离 ThreadData *td = static_cast (args); td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_); delete td; return nullptr; } void loop() { while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4. 获取连接, accept 的返回值是一个新的socket fd ?? int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5 提供服务, echo -> 小写 -> 大写 // 5.2 v2 版本 -- 多线程 // 这里不需要进行关闭文件描述符吗??不需要啦 // 多线程是会共享文件描述符表的! ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this); pthread_t tid; pthread_create(&tid, nullptr, threadRoutine, (void*)td); } } // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if(strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 可以进行大小写转化了 for(int i = 0; i < s; i++) { if(isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } // 只要走到这里,一定是client退出了,服务到此结束 close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏! logMessage(DEBUG, "server close %d done", sock); } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8081 127.0.0.1\n" << std::endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if(argc != 2 && argc != 3 ) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if(argc == 3) ip = argv[2]; ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; } clientTcp.cc文件:
#include "util.hpp" // 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!! // 3. 需要listen吗?不需要的! // 4. 需要accept吗?不需要的! volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1. 创建socket SOCK_STREAM int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,connect 会自动帮我们进行bind! if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << sock << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(sock); return 0; }
log.hpp文件:
#pragma once #include
#include #include #include #include #include #include #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr:stdout; fprintf(out, "%s | %u | %s | %s\n", \ log_level[level], \ (unsigned int)time(nullptr),\ name == nullptr ? "unknow":name,\ logInfo); } util.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #define SOCKET_ERR 1 #define BIND_ERR 2 #define LISTEN_ERR 3 #define USAGE_ERR 4 #define CONN_ERR 5 #define BUFFER_SIZE 1024 Makefile文件:
.PHONY:all all:clientTcp serverTcp clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcp:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcp clientTcp
注:
1.创建一个ThreadData结构体,保存客户端网络相关的所有信息。这里与子进程一样,每有一个客户端进行连接,父进程就创建一个线程来对对应客户端提供服务。与子进程一样,主进程也不能阻塞式的等待线程,存在等待回收线程问题。这里的等待回收线程问题使用线程分离就可以很好的解决了。
这里的threadRoutine函数是类内函数,默认带有this指针参数,因此我们将threadRoutine函数设置为static的,但是static的threadRoutine函数无法访问某个类对象的成员变量,这里显式的传递this类对象即可。
2.这里不需要也不能使用close函数关闭文件描述符,因为多线程是会共享文件描述符表的。
3.对于服务端,当一个客户端连接来了才去给客户端创建线程或子进程提供服务,这样效率较低,我们可以用池化技术来解决该问题。
创建serverTcp.cc文件,写入下图一所示的代码,创建clientTcp.cc文件,写入下图二所示的代码,创建log.hpp文件,写入下图三所示的代码,创建util.hpp文件,写入下图四所示的代码,创建ThreadPool.hpp文件,写入下图五所示的代码,创建Task.hpp文件,写入下图六所示的代码,创建Lock.hpp文件,写入下图七所示的代码,创建Makefile文件,写入下图八所示的代码,
使用make命令生成serverTcp和clientTcp可执行程序,创建两个选项卡,一个选项卡使用./udpServer 8081命令运行serverTcp可执行程序,一个选项卡使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,再创建一个选项卡,使用./clientTcp 127.0.0.1 8081命令运行clientTcp可执行程序,如下图九所示。
serverTcp.cc文件:
#include "util.hpp" #include "Task.hpp" #include "ThreadPool.hpp" #include
#include #include #include class ServerTcp; // 申明一下ServerTcp // 大小写转化服务 // TCP && UDP: 支持全双工 void transService(int sock, const std::string &clientIp, uint16_t clientPort) { assert(sock >= 0); assert(!clientIp.empty()); assert(clientPort >= 1024); char inbuffer[BUFFER_SIZE]; while (true) { ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串 if (s > 0) { // read success inbuffer[s] = '\0'; if (strcasecmp(inbuffer, "quit") == 0) { logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); // 可以进行大小写转化了 for (int i = 0; i < s; i++) { if (isalpha(inbuffer[i]) && islower(inbuffer[i])) inbuffer[i] = toupper(inbuffer[i]); } logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer); write(sock, inbuffer, strlen(inbuffer)); } else if (s == 0) { // pipe: 读端一直在读,写端不写了,并且关闭了写端,读端会如何?s == 0,代表对端关闭 // s == 0: 代表对方关闭,client 退出 logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort); break; } else { logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno)); break; } } // 只要走到这里,一定是client退出了,服务到此结束 close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏! logMessage(DEBUG, "server close %d done", sock); } class ThreadData { public: uint16_t clientPort_; std::string clinetIp_; int sock_; ServerTcp *this_; public: ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts) : clientPort_(port), clinetIp_(ip), sock_(sock), this_(ts) { } }; class ServerTcp { public: ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1), tp_(nullptr) { } ~ServerTcp() { } public: void init() { // 1. 创建socket listenSock_ = socket(PF_INET, SOCK_STREAM, 0); if (listenSock_ < 0) { logMessage(FATAL, "socket: %s", strerror(errno)); exit(SOCKET_ERR); } logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_); // 2. bind绑定 // 2.1 填充服务器信息 struct sockaddr_in local; // 用户栈 memset(&local, 0, sizeof local); local.sin_family = PF_INET; local.sin_port = htons(port_); ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr)); // 2.2 本地socket信息,写入sock_对应的内核区域 if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind: %s", strerror(errno)); exit(BIND_ERR); } logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_); // 3. 监听socket,为何要监听呢?tcp是面向连接的! if (listen(listenSock_, 5 /*后面再说*/) < 0) { logMessage(FATAL, "listen: %s", strerror(errno)); exit(LISTEN_ERR); } logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_); // 运行别人来连接你了 // 4. 加载线程池 tp_ = ThreadPool ::getInstance(); } void loop() { tp_->start(); logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum()); while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); // 4. 获取连接, accept 的返回值是一个新的socket fd ?? // 4.1 listenSock_: 监听 && 获取新的链接-> sock // 4.2 serviceSock: 给用户提供新的socket服务 int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len); if (serviceSock < 0) { // 获取链接失败 logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock); continue; } // 4.1 获取客户端基本信息 uint16_t peerPort = ntohs(peer.sin_port); std::string peerIp = inet_ntoa(peer.sin_addr); logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d", strerror(errno), peerIp.c_str(), peerPort, serviceSock); // 5 提供服务, echo -> 小写 -> 大写 // 5.3 v3.2 Task t(serviceSock, peerIp, peerPort, transService); tp_->push(t); } } private: // sock int listenSock_; // port uint16_t port_; // ip std::string ip_; // 引入线程池 ThreadPool *tp_; }; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl; std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl; } // ./ServerTcp local_port local_ip int main(int argc, char *argv[]) { if (argc != 2 && argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); std::string ip; if (argc == 3) ip = argv[2]; ServerTcp svr(port, ip); svr.init(); svr.loop(); return 0; } clientTcp.cc文件:
#include "util.hpp" // 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!! // 3. 需要listen吗?不需要的! // 4. 需要accept吗?不需要的! volatile bool quit = false; static void Usage(std::string proc) { std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl; std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n" << std::endl; } // ./clientTcp serverIp serverPort int main(int argc, char *argv[]) { if (argc != 3) { Usage(argv[0]); exit(USAGE_ERR); } std::string serverIp = argv[1]; uint16_t serverPort = atoi(argv[2]); // 1. 创建socket SOCK_STREAM int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { std::cerr << "socket: " << strerror(errno) << std::endl; exit(SOCKET_ERR); } // 2. connect,发起链接请求,你想谁发起请求呢??当然是向服务器发起请求喽 // 2.1 先填充需要连接的远端主机的基本信息 struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverPort); inet_aton(serverIp.c_str(), &server.sin_addr); // 2.2 发起请求,connect 会自动帮我们进行bind! if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "connect: " << strerror(errno) << std::endl; exit(CONN_ERR); } std::cout << "info : connect success: " << sock << std::endl; std::string message; while (!quit) { message.clear(); std::cout << "请输入你的消息>>> "; std::getline(std::cin, message); // 结尾不会有\n if (strcasecmp(message.c_str(), "quit") == 0) quit = true; ssize_t s = write(sock, message.c_str(), message.size()); if (s > 0) { message.resize(1024); ssize_t s = read(sock, (char *)(message.c_str()), 1024); if (s > 0) message[s] = 0; std::cout << "Server Echo>>> " << message << std::endl; } else if (s <= 0) { break; } } close(sock); return 0; }
log.hpp文件:
#pragma once #include
#include #include #include #include #include #include #define DEBUG 0 #define NOTICE 1 #define WARINING 2 #define FATAL 3 const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"}; // logMessage(DEBUG, "%d", 10); void logMessage(int level, const char *format, ...) { assert(level >= DEBUG); assert(level <= FATAL); char *name = getenv("USER"); char logInfo[1024]; va_list ap; // ap -> char* va_start(ap, format); vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); va_end(ap); // ap = NULL FILE *out = (level == FATAL) ? stderr:stdout; fprintf(out, "%s | %u | %s | %s\n", \ log_level[level], \ (unsigned int)time(nullptr),\ name == nullptr ? "unknow":name,\ logInfo); } util.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include #include #include #include #include "log.hpp" #define SOCKET_ERR 1 #define BIND_ERR 2 #define LISTEN_ERR 3 #define USAGE_ERR 4 #define CONN_ERR 5 #define BUFFER_SIZE 1024 ThreadPool.hpp文件:
#pragma once #include
#include #include #include #include #include #include #include #include "Lock.hpp" using namespace std; int gThreadNum = 5; template class ThreadPool { private: ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false) { assert(threadNum_ > 0); pthread_mutex_init(&mutex_, nullptr); pthread_cond_init(&cond_, nullptr); } ThreadPool(const ThreadPool &) = delete; void operator=(const ThreadPool &) = delete; public: static ThreadPool *getInstance() { static Mutex mutex; if (nullptr == instance) //仅仅是过滤重复的判断 { LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁 if (nullptr == instance) { instance = new ThreadPool (); } } return instance; } //类内成员, 成员函数,都有默认参数this static void *threadRoutine(void *args) { pthread_detach(pthread_self()); ThreadPool *tp = static_cast *>(args); while (1) { tp->lockQueue(); while (!tp->haveTask()) { tp->waitForTask(); } //这个任务就被拿到了线程的上下文中 T t = tp->pop(); tp->unlockQueue(); t(); // 让指定的先处理这个任务 } } void start() { assert(!isStart_); for (int i = 0; i < threadNum_; i++) { pthread_t temp; pthread_create(&temp, nullptr, threadRoutine, this); } isStart_ = true; } void push(const T &in) { lockQueue(); taskQueue_.push(in); choiceThreadForHandler(); unlockQueue(); } ~ThreadPool() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } int threadNum() { return threadNum_; } private: void lockQueue() { pthread_mutex_lock(&mutex_); } void unlockQueue() { pthread_mutex_unlock(&mutex_); } bool haveTask() { return !taskQueue_.empty(); } void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } void choiceThreadForHandler() { pthread_cond_signal(&cond_); } T pop() { T temp = taskQueue_.front(); taskQueue_.pop(); return temp; } private: bool isStart_; int threadNum_; queue taskQueue_; pthread_mutex_t mutex_; pthread_cond_t cond_; static ThreadPool *instance; // const static int a = 100; }; template ThreadPool *ThreadPool ::instance = nullptr; Task.hpp文件:
#pragma once #include
#include #include #include #include "log.hpp" class Task { public: //等价于 // typedef std::function callback_t; using callback_t = std::function ; private: int sock_; // 给用户提供IO服务的sock uint16_t port_; // client port std::string ip_; // client ip callback_t func_; // 回调方法 public: Task():sock_(-1), port_(-1) {} Task(int sock, std::string ip, uint16_t port, callback_t func) : sock_(sock), ip_(ip), port_(port), func_(func) {} void operator () () { logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\ pthread_self(), ip_.c_str(), port_); func_(sock_, ip_, port_); logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\ pthread_self(), ip_.c_str(), port_); } ~Task() {} }; Lock.hpp文件:
#pragma once #include
#include class Mutex { public: Mutex() { pthread_mutex_init(&lock_, nullptr); } void lock() { pthread_mutex_lock(&lock_); } void unlock() { pthread_mutex_unlock(&lock_); } ~Mutex() { pthread_mutex_destroy(&lock_); } private: pthread_mutex_t lock_; }; class LockGuard { public: LockGuard(Mutex *mutex) : mutex_(mutex) { mutex_->lock(); std::cout << "加锁成功..." << std::endl; } ~LockGuard() { mutex_->unlock(); std::cout << "解锁成功...." << std::endl; } private: Mutex *mutex_; }; Makefile文件:
.PHONY:all all:clientTcp serverTcp clientTcp: clientTcp.cc g++ -o $@ $^ -std=c++11 serverTcp:serverTcp.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f serverTcp clientTcp
注:
1.在服务端ServerTcp中新增线程池指针成员变量tp_,在init初始化的时候getInstance加载线程池,在loop函数中start启动线程池。loop函数中提供服务部分,每有一个客户端连接需要提供服务,就创建一个对应客户端的Task服务任务,然后将任务push放进线程池即可。
在Task.hpp文件中,定义了sock_、ip_、port_成员变量用来获取对应服务任务服务的客户端网络信息,类型重定义了一个回调函数方法callback_t并利用callback_t定义了func_成员变量用来获取服务方法(类型重定义回调函数方法callback_t时 using callback_t = std::function
与 typedef std::function callback_t 是等价的),定义了一个仿函数来执行回调函数方法。 在ThreadPool.hpp文件的线程池中,线程执行threadRoutine函数,threadRoutine函数中使用t()调用Task类的仿函数来执行回调函数,对客户端提供服务。
2.这里我们线程池中只创建了5个线程,而我们设置了对应的任务是死循环,因此这里线程池只能同时服务五个客户端。