目录
1. TCP网络编程
1.1 前期代码
log.hpp
tcp_server.cc
1.2 accept和单进程版代码
1.3 多进程版strat代码
1.4 client.cc客户端
1.5 多进程版strat代码改进+多线程
1.6 线程池版本
Task.hpp
lockGuard.hpp
thread.hpp
threadPool.hpp
多个回调任务
tcp_client.cc
tcp_server.hpp
2. 笔试选择题
答案及解析
本篇完。
1. TCP网络编程
框架和前面udp通信一样,接口函数上一篇也讲了,这里直接放一部分代码:
1.1 前期代码
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...) // 可变参数
{
#ifndef DEBUG_SHOW
if(level== DEBUG)
{
return;
}
#endif
char stdBuffer[1024]; // 标准日志部分
time_t timestamp = time(nullptr); // 获取时间戳
// struct tm *localtime = localtime(×tamp); // 转化麻烦就不写了
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; // 自定义日志部分
va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof(logBuffer), format, args);
va_end(args); // 相当于ap=nullptr
printf("%s%s\n", stdBuffer, logBuffer);
// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
client.cc
#include <iostream>
int main()
{
return 0;
}
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
class TcpServer
{
protected:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:
TcpServer(uint16_t port, std::string ip="")
:_listensock(-1)
, _port(port)
, _ip(ip)
{}
void initServer()
{
// 1. 创建socket -- 进程和文件
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if(_listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start()
{
while(true)
{
sleep(7);
}
}
~TcpServer()
{}
protected:
uint16_t _port;
std::string _ip;
int _listensock;
};
tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(port));
svr->initServer();
svr->start();
return 0;
}
编译运行:
此时程序就跑起来了。
1.2 accept和单进程版代码
再写start函数:
void start()
{
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
service(servicesock, client_ip, client_port);
}
}
第4步获取链接,写在start函数中,如上图所示,使用accept来接收客户端的连接请求,有点像udp中的recvfrom一样,只是accept是用来接收套接字的连接请求,而recvfrom是接收套接字中的数据的。man accept:
accept系统调用的参数和recvfrom中的一样,如上图所示,accept的作用就是接收来自套接字中的连接请求,也就是来自客户端的连接请求。
设置为listen状态的套接字不用了通信,只是用来接收客户端的网络请求,具体体现在accept的返回值上。
第一步中创建的套接字就像是一个门童,使用accept来接收客户端的连接请求,如果有连接请求并且接收成功,那么会返回一个文件描述符fd。 这里的文件描述符sock和前面的_listensock不是一个东西,_listensock是我们创建的,是专门用来接收连接请求的,而accept返回的sock是操作系统在接收成功连接请求后新创建的套接字的文件描述符。 sock指向的文件描述符是服务端专门用来和客户端通信的,所以每有一个客户端向服务器发起连接请求,客户端接收成功够都会创建一个套接字用来一对一的提供服务。
如果accept接收连接请求失败,则返回-1,并且设置错误码。这里的失败并不是致命的,就像门童拉客一样,拉客失败也没有什么,继续进行下一次拉客就行。 所以accept失败也没有什么,继续接收下一个连接请求即可,所以在代码中,如果接收失败,使用了continue继续接收连接请求。
accept是阻塞执行的,在没有网络连接请求的时候,会阻塞等待,直到客户端的网络连接请求到来。
至此,进行tcp网络通信的所有准备工作已经做完,接下来就是进行具体的服务了,也就是读取客户端发送来的数据并做相应的处理了。看一下start在最后调用的service函数:
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
//echo server
char buffer[1024];
while(true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if(s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
如代码所示,就是服务器指向的具体服务函数。 客户端读取客户端发送来的数据时,是从accept返回的文件描述符sock指向的套接字中读取数据的,因为这个套接字是专门用来服务客户端的。
读取数据时,使用的是read系统调用,和读取普通文件一模一样。
数据读取成功后,做一些处理,先将读取的数据打印一下,加一个回显,再给客户端发送过去。
发送数据时,使用的是write系统调用,写入的也是sock指向的套接字,同样与向普通文件中写入数据一模一样。
在读取普通文件的时候,如果文件被读完了,read会返回0,表示文件的内容被读取完毕。 但是在使用read读取tcp套接字的时候,如果读取到0,表示客户端关闭了它的套接字,代表着客户端不再进行网络通信了,此时服务端就可以结束这次通信了,也就是将sock指向的套接字关闭。
这里再放下tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
//echo server
char buffer[1024];
while(true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if(s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
class TcpServer
{
protected:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
public:
TcpServer(uint16_t port, std::string ip="")
:_listensock(-1)
, _port(port)
, _ip(ip)
{}
void initServer()
{
// 1. 创建socket -- 进程和文件
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if(_listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start()
{
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(_servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
service(servicesock, client_ip, client_port);
}
}
~TcpServer()
{}
protected:
uint16_t _port;
std::string _ip;
int listensock;
};
编译运行:
telnet 是一个远程链接命令,这里切换到了root输入了这个下载指令:yum -y install telnet telnet-server xinetd,以后输入telnet 127.0.0.1 7070链接到启动了的程序,然后Ctrl+],再回车就能发消息了,最后Ctrl+]输入quit就退出了。
1.3 多进程版strat代码
只用改strat函数:
void start()
{
// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
// service(servicesock, client_ip, client_port);
// 2 version 2.0 -- 多进程版 --- 创建子进程
// 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
pid_t id = fork();
assert(id != -1);
if(id == 0) // 子进程
{
// 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
// 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
close(_listensock); // 关闭自己不需要的套接字
service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
exit(0); // 僵尸状态
}
// 父进程
close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
// 如果父进程关闭servicesock,会不会影响子进程?
// 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
// waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
}
}
编译运行:
此时就完整了多进程的第一个版本。
1.4 client.cc客户端
看看接口,man accept
如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,
accept()返回时传出客户端的地址和端口号。
addr是一个传输出型参数,如果给addr 参数传NULL,表示不关心客户端的地址。
addrlen参数是一个输入输出型参数(value-result argument),
输入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,
man connect
客户端需要调用connect()连接服务器;connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址;connect()成功返回0,出错返回-1。
client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}
// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client 要不要bind呢?不需要显示的bind,但是一定是需要port
// 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
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) // 比UDP多了这一步
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl; // 到这链接成功了
while (true)
{
std::string line;
std::cout << "请输入# ";
std::getline(std::cin, line);
send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回显# " << buffer << std::endl;
}
else // 关闭链接或者读取失败
{
break;
}
}
close(sock);
return 0;
}
编译运行:
1.5 多进程版strat代码改进+多线程
直接放代码:
void start()
{
// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
// service(servicesock, client_ip, client_port);
// // 2 version 2.0 -- 多进程版 --- 创建子进程
// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
// pid_t id = fork();
// assert(id != -1);
// if(id == 0) // 子进程
// {
// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
// close(_listensock); // 关闭自己不需要的套接字
// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
// exit(0); // 僵尸状态
// }
// // 父进程
// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
// // 如果父进程关闭servicesock,会不会影响子进程?
// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// version2.1 -- 多进程版 -- version 2.0 改版
pid_t id = fork();
if(id == 0)
{
// 子进程
close(_listensock);
if(fork() > 0) // 再创建子进程,子进程本身
exit(0); //子进程本身立即退出
// 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
service(servicesock, client_ip, client_port);
exit(0);
}
// 父进程
waitpid(id, nullptr, 0); // 不会阻塞
close(servicesock);
}
}
还是和1.4 一样的效果,创建进程的成本是很高的,所以再改成多线程版,直接放代码:
(给Makefile加上-lpthread)
这是改的部分:
tcp_server.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
//echo server
char buffer[1024];
while(true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if(s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
class ThreadData
{
public:
int _sock;
std::string _ip;
uint16_t _port;
};
class TcpServer
{
protected:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
static void *threadRoutine(void *args) // 加上static就没this指针了
{
pthread_detach(pthread_self()); // 线程分离
ThreadData *td = static_cast<ThreadData *>(args);
service(td->_sock, td->_ip, td->_port);
delete td;
return nullptr;
}
public:
TcpServer(uint16_t port, std::string ip="")
:_listensock(-1)
, _port(port)
, _ip(ip)
{}
void initServer()
{
// 1. 创建socket -- 进程和文件
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if(_listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start()
{
// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
// service(servicesock, client_ip, client_port);
// // 2 version 2.0 -- 多进程版 --- 创建子进程
// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
// pid_t id = fork();
// assert(id != -1);
// if(id == 0) // 子进程
// {
// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
// close(_listensock); // 关闭自己不需要的套接字
// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
// exit(0); // 僵尸状态
// }
// // 父进程
// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
// // 如果父进程关闭servicesock,会不会影响子进程?
// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// // version2.1 -- 多进程版 -- version 2.0 改版
// pid_t id = fork();
// if(id == 0)
// {
// // 子进程
// close(_listensock);
// if(fork() > 0) // 再创建子进程,子进程本身
// exit(0); //子进程本身立即退出
// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
// service(servicesock, client_ip, client_port);
// exit(0);
// }
// // 父进程
// waitpid(id, nullptr, 0); // 不会阻塞
// close(servicesock);
// version 3 --- 多线程版本
// 创建进程的成本是很高的,所以再改成多线程版(不封装了)
ThreadData *td = new ThreadData();
td->_sock = servicesock;
td->_ip = client_ip;
td->_port = client_port;
pthread_t tid;
// 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类
}
}
~TcpServer()
{}
protected:
uint16_t _port;
std::string _ip;
int _listensock;
};
编译运行:还是和上面一样的效果
1.6 线程池版本
把以前写的线程池和有关的代码拷过来:(只有Task.hpp要改一改)
Task.hpp
#pragma once
#include <functional>
// typedef std::function<void (int , const std::string &, const uint16_t &, const std::string &)> func_t;
using func_t = std::function<void (int , const std::string &, const uint16_t &, const std::string &)>; // 和上一行一样的效果
class Task
{
public:
Task()
{}
Task(int sock, const std::string ip, uint16_t port, func_t func)
: _sock(sock)
, _ip(ip)
, _port(port)
, _func(func)
{}
void operator ()(const std::string &name)
{
_func(_sock, _ip, _port, name);
}
public:
int _sock;
std::string _ip;
uint16_t _port;
// int type;
func_t _func;
};
lockGuard.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* mtx)
:_pmtx(mtx)
{}
void lock()
{
pthread_mutex_lock(_pmtx);
// std::cout << "进行加锁成功" << std::endl;
}
void unlock()
{
pthread_mutex_unlock(_pmtx);
// std::cout << "进行解锁成功" << std::endl;
}
~Mutex()
{}
protected:
pthread_mutex_t* _pmtx;
};
class lockGuard // RAII风格的加锁方式
{
public:
lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
:_mtx(mtx)
{
_mtx.lock();
}
~lockGuard()
{
_mtx.unlock();
}
protected:
Mutex _mtx;
};
thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_t
class ThreadData // 线程数据
{
public:
void *_args; // 真实参数
std::string _name; // 名字
};
class Thread // 封装的线程
{
public:
Thread(int num, fun_t callback, void *args)
: _func(callback) // 回调函数
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer
_name = nameBuffer;
_tdata._args = args; // 线程构造时把参数和名字带给线程数据
_tdata._name = _name;
}
void start() // 启动线程
{
pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据
}
void join() // join自己
{
pthread_join(_tid, nullptr);
}
std::string name() // 返回线程名
{
return _name;
}
~Thread() // 析构什么也不做
{}
protected:
std::string _name; // 线程名字
pthread_t _tid; // 线程tid
fun_t _func; // 线程要执行的函数
ThreadData _tdata; // 线程数据
};
threadPool.hpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
const int g_thread_num = 7;
// 线程池->有一批线程,一批任务,有任务push有任务pop,本质是: 生产消费模型
template<class T>
class ThreadPool
{
private:
ThreadPool(int thread_num = g_thread_num)
:_num(thread_num)
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
for(int i = 1; i <= _num; ++i)
{
_threads.push_back(new Thread(i, routine, this));
}
}
ThreadPool(const ThreadPool<T> &other) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;
public:
static ThreadPool<T> *getThreadPool(int num = g_thread_num) // 多线程使用单例的过程
{
// 可以有效减少未来必定要进行加锁检测的问题
// 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为
// 如果这里不加if,未来任何一个线程想获取单例,都必须调用getThreadPool接口
// 一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的
if (nullptr == thread_ptr)
{
lockGuard lockguard(&mutex);
// pthread_mutex_lock(&mutex);
if (nullptr == thread_ptr)
{
thread_ptr = new ThreadPool<T>(num);
}
// pthread_mutex_unlock(&mutex);
}
return thread_ptr;
}
void run() // 1. 线程池的整体启动
{
for (auto &iter : _threads)
{
iter->start();
std::cout << iter->name() << " 启动成功" << std::endl;
}
}
pthread_mutex_t *getMutex()
{
return &lock;
}
bool isEmpty()
{
return _task_queue.empty();
}
void waitCond() // 特定的条件变量下等待
{
pthread_cond_wait(&cond, &lock);
}
T getTask()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
static void *routine(void *args) // 每个线程启动后做的工作
{ // 类的成员函数有this指针 -> 两个参数 -> 类型不匹配 -> 所以加static
// 消费过程 -> 访问_task_queue -> 静态访问不了 -> 构造函数传this指针
ThreadData *td = (ThreadData *)args;
ThreadPool<T> *tp = (ThreadPool<T> *)td->_args;
while (true)
{
T task;
{
lockGuard lockguard(tp->getMutex()); // 出花括号自动调用析构,花括号里的接口全是加锁的
while (tp->isEmpty()) // 空就等待
{
tp->waitCond();
}
// 任务队列不为空,读取任务
task = tp->getTask(); // 是共享的-> 将任务从共享,拿到自己的私有空间
}
task(td->_name); // 告诉哪一个线程去处理这个任务就行了
}
}
void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务
{
lockGuard lockguard(&lock); // 加锁,执行完这个函数自动解锁
_task_queue.push(task); // 生产一个任务
pthread_cond_signal(&cond); // 唤醒一个线程
}
// void joins()
// {
// for (auto &iter : _threads)
// {
// iter->join();
// }
// }
~ThreadPool()
{
for (auto &iter : _threads)
{
// iter->join();
delete iter;
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
protected:
std::vector<Thread *> _threads; // 保存一堆线程的容器
int _num; // 线程的数量
std::queue<T> _task_queue; // 任务队列
pthread_mutex_t lock;
pthread_cond_t cond;
static ThreadPool<T> *thread_ptr; // 懒汉模式的单例对象指针
static pthread_mutex_t mutex; // 单例对象的锁
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr; // 定义初始化为空
template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER; // 定义锁
多个回调任务
改下tcp_server.hpp:
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>
#include "log.hpp"
#include "threadPool.hpp"
#include "Task.hpp"
// static void service(int sock, const std::string &clientip, const uint16_t &clientport)
// {
// //echo server
// char buffer[1024];
// while(true)
// {
// // read && write 可以直接被使用
// ssize_t s = read(sock, buffer, sizeof(buffer)-1);
// if(s > 0)
// {
// buffer[s] = 0; // 将发过来的数据当做字符串
// std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
// }
// else if(s == 0) // 对端关闭连接
// {
// logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
// break;
// }
// else
// {
// logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
// break;
// }
// write(sock, buffer, strlen(buffer));
// }
// }
static void service(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name) // 带上线程名的service
{
// echo server
char buffer[1024];
while (true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
close(sock);
}
// class ThreadData
// {
// public:
// int _sock;
// std::string _ip;
// uint16_t _port;
// };
class TcpServer
{
protected:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
// static void *threadRoutine(void *args) // 加上static就没this指针了
// {
// pthread_detach(pthread_self()); // 线程分离
// ThreadData *td = static_cast<ThreadData *>(args);
// service(td->_sock, td->_ip, td->_port);
// delete td;
// return nullptr;
// }
public:
TcpServer(uint16_t port, std::string ip="")
:_listensock(-1)
, _port(port)
, _ip(ip)
, _threadpool_ptr(ThreadPool<Task>::getThreadPool())
{}
void initServer()
{
// 1. 创建socket -- 进程和文件
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if(_listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start()
{
// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
_threadpool_ptr->run();
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
// service(servicesock, client_ip, client_port);
// // 2 version 2.0 -- 多进程版 --- 创建子进程
// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
// pid_t id = fork();
// assert(id != -1);
// if(id == 0) // 子进程
// {
// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
// close(_listensock); // 关闭自己不需要的套接字
// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
// exit(0); // 僵尸状态
// }
// // 父进程
// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
// // 如果父进程关闭servicesock,会不会影响子进程?
// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// // version2.1 -- 多进程版 -- version 2.0 改版
// pid_t id = fork();
// if(id == 0)
// {
// // 子进程
// close(_listensock);
// if(fork() > 0) // 再创建子进程,子进程本身
// exit(0); //子进程本身立即退出
// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
// service(servicesock, client_ip, client_port);
// exit(0);
// }
// // 父进程
// waitpid(id, nullptr, 0); // 不会阻塞
// close(servicesock);
// // version 3 --- 多线程版本
// // 创建进程的成本是很高的,所以再改成多线程版(不封装了)
// ThreadData *td = new ThreadData();
// td->_sock = servicesock;
// td->_ip = client_ip;
// td->_port = client_port;
// pthread_t tid;
// // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
// pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类
// verison4 --- 线程池版本
Task t(servicesock, client_ip, client_port, service);
_threadpool_ptr->pushTask(t);
}
}
~TcpServer()
{}
protected:
uint16_t _port;
std::string _ip;
int _listensock;
std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};
编译运行
完成了多个线程实现回显任务的情景。下面写一个回调的函数,实现小写字母转大写字母:
static void change(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// 一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
char buffer[1024];
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
std::string message;
char *start = buffer;
while(*start)
{
char c;
if(islower(*start))
{
c = toupper(*start);
}
else
{
c = *start;
}
message.push_back(c);
++start;
}
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
close(sock);
}
只需要改一下回调方法:
把client.cc改成循环的:
tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}
// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
bool alive = false;
int sock = 0;
std::string line;
while (true) // 客户端不断的链接
{
if (!alive)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client 要不要bind呢?不需要显示的bind,但是一定是需要port
// 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
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) // 比UDP多了这一步
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl; // 到这链接成功了
alive = true;
}
std::cout << "请输入# ";
std::getline(std::cin, line);
if (line == "quit")
break;
ssize_t s = send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
if (s > 0) // ssize_t有符号的整数
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回显# " << buffer << std::endl;
}
else if (s == 0)
{
alive = false;
close(sock);
}
}
else // 关闭链接或者读取失败
{
alive = false;
close(sock);
}
}
return 0;
}
/*
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client 要不要bind呢?不需要显示的bind,但是一定是需要port
// 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
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) // 比UDP多了这一步
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl; // 到这链接成功了
while (true)
{
std::string line;
std::cout << "请输入# ";
std::getline(std::cin, line);
send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
if (s > 0)
{
buffer[s] = 0;
std::cout << "server 回显# " << buffer << std::endl;
}
else // 关闭链接或者读取失败
{
break;
}
}
close(sock);
return 0;
}
*/
编译运行:
此时链接的时候没有把回显信息打印出来,不过先不改了。下面再弄一个类似英汉互译的:
static void dictOnline(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// 一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
char buffer[1024];
static std::unordered_map<std::string, std::string> dict = {
{"apple", "苹果"},
{"watermelon", "西瓜"},
{"banana", "香蕉"},
{"hard", "好难"}
};
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
std::string message;
auto iter = dict.find(buffer);
if(iter == dict.end())
message = "此字典找不到...";
else
message = iter->second;
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
close(sock);
}
编译运行:
完成运行,为了方便这里再放下tcp_server.hpp。
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>
#include "log.hpp"
#include "threadPool.hpp"
#include "Task.hpp"
#include <unordered_map>
// static void service(int sock, const std::string &clientip, const uint16_t &clientport)
// {
// //echo server
// char buffer[1024];
// while(true)
// {
// // read && write 可以直接被使用
// ssize_t s = read(sock, buffer, sizeof(buffer)-1);
// if(s > 0)
// {
// buffer[s] = 0; // 将发过来的数据当做字符串
// std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
// }
// else if(s == 0) // 对端关闭连接
// {
// logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
// break;
// }
// else
// {
// logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
// break;
// }
// write(sock, buffer, strlen(buffer));
// }
// }
static void service(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name) // 带上线程名的service
{
// echo server
char buffer[1024];
while (true)
{
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
close(sock);
}
static void change(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// 一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
char buffer[1024];
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
std::string message;
char *start = buffer;
while(*start)
{
char c;
if(islower(*start))
{
c = toupper(*start);
}
else
{
c = *start;
}
message.push_back(c);
++start;
}
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
close(sock);
}
static void dictOnline(int sock, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
// 一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
char buffer[1024];
static std::unordered_map<std::string, std::string> dict = {
{"apple", "苹果"},
{"watermelon", "西瓜"},
{"banana", "香蕉"},
{"hard", "好难"}
};
// read && write 可以直接被使用
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
std::string message;
auto iter = dict.find(buffer);
if(iter == dict.end())
message = "此字典找不到...";
else
message = iter->second;
write(sock, message.c_str(), message.size());
}
else if (s == 0) // 对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
}
else
{
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
}
close(sock);
}
// class ThreadData
// {
// public:
// int _sock;
// std::string _ip;
// uint16_t _port;
// };
class TcpServer
{
protected:
const static int gbacklog = 20; // listen的第二个参数,现在先不管
// static void *threadRoutine(void *args) // 加上static就没this指针了
// {
// pthread_detach(pthread_self()); // 线程分离
// ThreadData *td = static_cast<ThreadData *>(args);
// service(td->_sock, td->_ip, td->_port);
// delete td;
// return nullptr;
// }
public:
TcpServer(uint16_t port, std::string ip="")
:_listensock(-1)
, _port(port)
, _ip(ip)
, _threadpool_ptr(ThreadPool<Task>::getThreadPool())
{}
void initServer()
{
// 1. 创建socket -- 进程和文件
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
if(_listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3
// 2. bind -- 文件 + 网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start()
{
// 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
_threadpool_ptr->run();
while(true)
{
// 4. 获取连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
// servicesock(服务套接字,相当于此小区域专门给你负责的)
// 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
/*换行符*/servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务
// // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
// service(servicesock, client_ip, client_port);
// // 2 version 2.0 -- 多进程版 --- 创建子进程
// // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
// pid_t id = fork();
// assert(id != -1);
// if(id == 0) // 子进程
// {
// // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
// // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
// close(_listensock); // 关闭自己不需要的套接字
// service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
// exit(0); // 僵尸状态
// }
// // 父进程
// close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
// // 如果父进程关闭servicesock,会不会影响子进程?
// // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
// // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
// // version2.1 -- 多进程版 -- version 2.0 改版
// pid_t id = fork();
// if(id == 0)
// {
// // 子进程
// close(_listensock);
// if(fork() > 0) // 再创建子进程,子进程本身
// exit(0); //子进程本身立即退出
// // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
// service(servicesock, client_ip, client_port);
// exit(0);
// }
// // 父进程
// waitpid(id, nullptr, 0); // 不会阻塞
// close(servicesock);
// // version 3 --- 多线程版本
// // 创建进程的成本是很高的,所以再改成多线程版(不封装了)
// ThreadData *td = new ThreadData();
// td->_sock = servicesock;
// td->_ip = client_ip;
// td->_port = client_port;
// pthread_t tid;
// // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
// pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类
// verison4 --- 线程池版本
// Task t(servicesock, client_ip, client_port, service);
// Task t(servicesock, client_ip, client_port, change);
Task t(servicesock, client_ip, client_port, dictOnline);
_threadpool_ptr->pushTask(t);
}
}
~TcpServer()
{}
protected:
uint16_t _port;
std::string _ip;
int _listensock;
std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};
2. 笔试选择题
1. 在网络字节序中,所谓”小端”(little endian)说法正确的是( )
A.高字节数据存放在低地址处,低字节数据存放在高地址处
B.低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处
C.和编译器相关
D.上述答案都不正确
2. 当一个UDP报文道达目的主机时,操作系统使用( )选择正确的socket.
A.源IP地址
B.源端口号
C.目的端口号
D.目的IP地址
3. 以下有关于端口号的说法错误的是()
A.tcp的最大端口号是65535
B.端口号是一个2字节16位的整数
C.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
D.一个进程至多只能绑定一个端口
4. 【多选题】socket编程中经常需要进行字节序列的转换,下列哪几个函数是将网络字节序列转换为主机字节序列?()
A.htons
B.ntohs
C.htonl
D.ntohl
5. 【多选题】Socket,即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口。socket的使用类型主要有()
A.基于TCP协议,采用流方式,提供可靠的字节流服务
B.基于IP协议,采用流数据提供数据网络发送服务
C.基于HTTP协议,采用数据包方式提供可靠的数据包装服务
D.基于UDP协议,采用数据报方式提供数据打包发送服务
6. 下列有关Socket的说法,错误的是()
A.Socket用于描述IP地址和端口,是一个通信链的句柄
B.Socket通信必须建立连接
C.Socket客户端的端口是不固定的
D.Socket服务端的端口是固定的
答案及解析
1. B
小端:低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处
大端:高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址
网络字节序为大端字节序
2. C
IP地址用于标识主机,端口号用于标识主机上的对应网络通信socket
因此正确选项为C选项,通过目的端口号来区分是哪个socket
3. D
端口号是一个无符号16位的整数,用于表示主机上的网络通信socket,因为是16位,因此最大端口号是65535(从0开始)
IP地址可以在网络当中标识一台主机,端口号可以在主机当中标识一个进程(socket会关联到对应进程)
一个进程当中对于端口的绑定是和socket强相关,理论上该进程如果创建多个socket,可以给每一个socket都进行绑定一个端口
基于以上理解,错误选项为:D (一个进程可能会创建多个socket,因此有可能会绑定多个端口)
4. BD
函数名称解析:
- n 对应是 network
- h 对应是 host
- s 对应是 short
- l 对应是 long
因此网络字节序到主机字节序的转换是 ntoh
5. AD
TCP协议,采用流方式,SOCK_STREAM, 可靠
UDP协议,采用数据报方式,SOCK_DGRAM, 不可靠
HTTP协议是应用层协议,在传输层基于TCP协议实现, (可靠是TCP提供的,TCP提供字节流传输)
IP协议是网络层协议,TCP和UDP协议在网络层都是基于IP协议的。(实现数据报传输)
6. B
A正确:概念性理解,socket就是一条通信的句柄
B错误:socket 可以基于TCP 面向连接 也可以基于UDP无连接
C正确:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定
D正确:服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了
本篇完。
下一篇:网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)。