网站域名使用方法,做网站线稿软件有哪些,wordpress mysql 配置,手机活动网站模板 作者#xff1a;დ旧言~ 座右铭#xff1a;松树千年终是朽#xff0c;槿花一日自为荣。 目标#xff1a;TCP网络服务器简单模拟实现。 毒鸡汤#xff1a;有些事情#xff0c;总是不明白#xff0c;所以我不会坚持。早安! 专栏选自#xff1a;… 作者დ旧言~ 座右铭松树千年终是朽槿花一日自为荣。 目标TCP网络服务器简单模拟实现。 毒鸡汤有些事情总是不明白所以我不会坚持。早安! 专栏选自网络 望小伙伴们点赞收藏✨加关注哟 一、前言
前面我们已经学习了网络的基础知识对网络的基本框架已有认识算是初步认识到网络了如果上期我们的学习网络是步入基础知识那么这次学习的板块就是基础知识的实践我们今天的板块是学习网络重要之一学习完这个板块对虚幻的网络就不再迷茫 二、主体
学习【网络】套接字编程——TCP通信咱们按照下面的图解 2.1 程序结构
分别实现客户端与服务器客户端向服务器发送消息服务器收到消息后回响给客户端有点类似于 echo 指令 这个程序我们已经基于 UDP 协议实现过了换成 TCP 协议实现时程序的结构是没有变化的同样需要 server.hpp、server.cc、client.hpp、client.cc 这几个文件 创建 server.hpp 服务器头文件 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.hnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0;//默认IPenum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR};class TcpServer{public:TcpServer( const uint16_t port default_port,const std::string ip default_ip):ip_(ip),port_(port){}~TcpServer(){}// 初始化服务器void InitServer(){}// 启动服务器void StartServer(){}private:int sock_; // 套接字存疑uint16_t port_; // 端口号std::string ip_;//ip地址};
} 创建 server.cc 服务器源文件 #include string
#include vector
#include memory // 智能指针相关头文件
#include cstdio
#include server.hppusing namespace std;
using namespace nt_server;//业务处理函数
std::string ExecCommand(const std::string request)
{ return request;
}void Usage(const char* program)
{cout Usage: endl;cout \t program ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrTcpServer usvr (new TcpServer(port));usvr-InitServer();usvr-StartServer();return 0;
} 创建 client.hpp 客户端头文件 #pragma once#include iostream
#include string
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include err.hppnamespace nt_client
{enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR};class TcpClient{public:TcpClient(const std::string ip, const uint16_t port):server_ip_(ip), server_port_(port){}~TcpClient(){}// 初始化客户端void InitClient(){}// 启动客户端void StartClient(){}private:int sock_; // 套接字std::string server_ip_; // 服务器IPuint16_t server_port_; // 服务器端口号};
} 创建 client.cc 客户端源文件 #include memory
#include client.hppusing namespace std;
using namespace nt_client;void Usage(const char *program)
{cout Usage: endl;cout \t program ServerIP ServerPort endl;
}int main(int argc, char *argv[])
{if (argc ! 3){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}// 服务器IP与端口号string ip(argv[1]);uint16_t port stoi(argv[2]);unique_ptrTcpClient usvr(new TcpClient(ip, port));usvr-InitClient();usvr-StartClient();return 0;
} 创建 Makefile 文件 .PHONY:all
all:server clientserver:server.ccg -o $ $^ -stdc11client:client.ccg -o $ $^ -stdc11.PHONY:clean
clean:rm -rf server client
2.2 Tcp Server 端代码 2.2.1 socket、bind - 初始化服务端
说明
在使用 socket 函数创建套接字时UDP 协议需要指定参数2为 SOCK_DGRAMTCP 协议则是指定参数2为 SOCK_STREAM。
代码呈现 server.hpp的初始化部分 // 初始化服务器void InitServer(){// 1.创建套接字sock_ socket(AF_INET, SOCK_STREAM, 0);if (sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if(bind(sock_,(const sockaddr *)local, sizeof(local))0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}// 3.TODO}
解释说明
在绑定端口号时一定需要把主机序列转换为网络序列。发送信息阶段recvfrom / sendto 等函数会自动将需要发送的信息转换为网络序列接收信息时同样会将其转换为主机序列所以不需要手动转换。
总结
TCP是面向连接的服务器一般是比较被动的没人访问这个服务器只能干等着而且也不能退出。就像你是一家餐馆的老板你只能在餐馆里被动的等待顾客的到来顾客什么时候来你也不知道。服务器要一直要处于等待链接到来的状态——监听状态。
2.2.2 listen - 监听一个套接字
语法
#include sys/types.h /* See NOTES */
#include sys/socket.hint listen(int sockfd, int backlog);使用说明
listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字被动等待客户端的连接。所谓被动监听是指当没有客户端请求时套接字处于“睡眠”状态只有当接收到客户端请求时套接字才会被“唤醒”来响应请求。
参数说明
int sockfd服务端的socket也就是socket函数创建的标识绑定的未连接的套接字的描述符。int backlogbacklog 为请求队列的最大长度。
细节说明
listen() 只是让套接字处于监听状态并没有接收请求。接收请求需要使用 accept() 函数。
代码呈现 server.hpp的初始化服务器部分 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#includecstringnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog5;//请求队列的最大长度enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer{public:TcpServer( const uint16_t port default_port,const std::string ip default_ip): ip_(ip), port_(port){}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字sock_ socket(AF_INET, SOCK_STREAM, 0);if (sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if(bind(sock_,(const sockaddr *)local, sizeof(local))0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if(listen(sock_,backlog)0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){for(;;){std::coutTCP SERVER is running.....std::endl;sleep(1);}}private:int sock_; // 套接字存疑uint16_t port_; // 端口号std::string ip_; // ip地址};
}
2.2.3 accept - 获取一个新连接
语法
#include sys/types.h /* See NOTES */
#include sys/socket.hint accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);使用说明
accept() 会阻塞程序执行后面代码不能被执行直到有新的请求到来。accept() 返回一个新的套接字来和客户端通信addr 保存了客户端的IP地址和端口号而 sockfd 是服务器端的套接字大家注意区分。后面和客户端通信时要使用这个新生成的套接字而不是原来服务器端的套接字。
参数说明
sockfd为服务器端套接字。addrsockaddr_in 结构体变量。addrlen参数 addr 的长度可由 sizeof() 求得。addr 与 addrlen是一个 输入输出型 参数类似于 recvfrom 中的参数。
基于TCP连接的服务器端为什么需要用两个套接字
在服务器端socket()返回的套接字用于监听listen和接受accept客户端的连接请求。这个套接字不能用于与客户端之间发送和接收数据。当某个客户端断开连接、或者是与某个客户端的通信完成之后服务器端需要关闭用于与该客户端通信的套接字。
代码呈现 server.hpp的StartServer的内容 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#includecstring
#includeunistd.hnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog5;//请求队列的最大长度enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer{public:TcpServer( const uint16_t port default_port,const std::string ip default_ip): ip_(ip), port_(port){}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if(bind(listen_sock_,(const sockaddr *)local, sizeof(local))0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if(listen(listen_sock_,backlog)0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){for(;;){//1.获取新连接struct sockaddr_in client;socklen_t lensizeof(client);int accept_socketaccept(listen_sock_,(struct sockaddr*)client,len);if(accept_socket0){std::coutaccept failedstd::endl;continue;}std::coutget a new link...,sockfd:accept_socketstd::endl;//2.根据新连接来进行通信}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址};
}
测试工具--telnet 安装 sudo yum install telnet 功能说明 telnet是一种用于远程登录的网络协议可以将本地计算机链接到远程主机。Linux提供了telnet命令它允许用户在本地计算机上通过telnet协议连接到远程主机并执行各种操作。使用telnet命令可以建立客户端与服务器之间的虚拟终端连接这使得用户可以通过网络远程登录到其他计算机并在远程计算机上执行命令就像直接在本地计算机上操作一样。 使用说明 telnet [选项] [主机名或IP地址] [端口号]参数说明 -l 用户名指定用户名进行登录。-p 端口号指定要连接的远程主机端口号。-E在telnet会话中打开字符转义模式。-e 字符指定telnet会话中的转义字符。-r在执行用户登录之前不要求用户名。-K在连接到telnet会话时要求密码。
2.2.4 read - 从套接字中读取数据
功能说明
因为 TCP 是面向字节流的所以可以直接使用 read 系统调用去读取数据。如果客户端退出了那么 read 会读到0此时需要把之前 accept 返回的 sockfd 关闭防止误操作造成意想不到的结果。
2.2.5 write - 向套接字中进行写入
使用
同理向套接字中进行写入时直接使用 write 系统调用即可。服务端在收到客户端的数据后先进行加工处理然后再进行写入上面 if(n 0) 后面就是写入的代码。唯一需要注意的就是如果在写入前或者正在写入的过程中client 端退出了此时客户端与服务器之间的连接就断了此时客户端如果进行写入操作可能会导致整个服务端崩掉。这和管道类似读端关闭写端继续写操作系统会给写端发送 13 号信号将写端 kill 调为了避免这种情况我们需要在服务器启动的时候将 13 号新号进行捕捉。
2.2.6 总代码呈现 server.hpp
#pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functionalnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog5;//请求队列的最大长度using func_t std::functionstd::string(std::string); // 回调函数类型enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer{public:TcpServer(const func_t func,const uint16_t port default_port,const std::string ip default_ip): ip_(ip), port_(port),func_(func)//注意这里要传1个业务处理函数{}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if(bind(listen_sock_,(const sockaddr *)local, sizeof(local))0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if(listen(listen_sock_,backlog)0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){for(;;){//1.获取新连接struct sockaddr_in client;socklen_t lensizeof(client);int accept_socketaccept(listen_sock_,(struct sockaddr*)client,len);if(accept_socket0){std::coutaccept failedstd::endl;continue;}//2.业务处理//2.1客户端信息存储uint16_t clientportntohs(client.sin_port);//客户端端口号char iptr[32];inet_ntop(AF_INET,(client.sin_addr),iptr,sizeof(iptr));//客户端IPstd::cout Server accept iptr - clientport accept_socket from listen_sock_ success! std::endl;//2.2.业务处理Service(accept_socket,iptr,clientport); }}// 通信服务业务处理void Service(int sock, const std::string clientip, const uint16_t clientport){char buff[1024];std::string who clientip - std::to_string(clientport);while (true){ssize_t n read(sock, buff, sizeof(buff) - 1); // 预留 \0 的位置if (n 0){// 读取成功buff[n] \0;std::cout Server get: buff from who std::endl;std::string respond func_(buff); // 实际业务处理由上层指定// 发送给服务器write(sock, buff, strlen(buff));}else if (n 0){// 表示当前读取到文件末尾了结束读取std::cout Client who sock quit! std::endl;close(sock); // 关闭文件描述符break;}else{// 读取出问题暂时std::cerr Read Fail! strerror(errno) std::endl;close(sock); // 关闭文件描述符break;}}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址func_t func_;//业务处理函数};
} server.cc代码 #include string
#include vector
#include memory // 智能指针相关头文件
#include cstdio
#include server.hppusing namespace std;
using namespace nt_server;// 业务处理回调函数字符串回响
string echo(string request)
{return request;
}void Usage(const char* program)
{cout Usage: endl;cout \t program ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrTcpServer usvr (new TcpServer(echo,port));usvr-InitServer();usvr-StartServer();return 0;
}
2.3 Tcp Client 端代码
功能说明 client.cc代码 #include iostream
#include memory
#include client.hppusing namespace std;
using namespace nt_client;void Usage(const char* program)
{cout Usage: endl;cout \t program ServerIP ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 3){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型std::string ipargv[1];uint16_t port stoi(argv[2]);//将字符串转换成端口号unique_ptrTcpClient usvr (new TcpClient(ip,port));usvr-InitClient();usvr-StartClient();return 0;
}
2.3.1 socket - 初始化客户端
说明
对于客户端来说服务器的 IP 地址与端口号是两个不可或缺的元素因此在客户端类中server_ip 和 server_port 这两个成员是少不了的当然得有 socket 套接字。初始化客户端只需要干一件事创建套接字客户端是主动发起连接请求的一方也就意味着它不需要使用 listen 函数设置为监听状态。TCP版本是不需要我们手动写代码去bind的也是操作系统自己去自动bind的。
代码呈现 client.hpp 客户端头文件 #pragma once#include iostream
#include string
#include cstring
#include cerrno
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.hnamespace nt_client
{enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpClient{public:TcpClient(const std::string ip, const uint16_t port):server_ip_(ip), server_port_(port){}~TcpClient(){}// 初始化客户端void InitClient(){// 创建套接字sock_ socket(AF_INET, SOCK_STREAM, 0);if (sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Sock Succeess! sock_ std::endl;}// 启动客户端void StartClient(){}private:int sock_; // 套接字std::string server_ip_; // 服务器IPuint16_t server_port_; // 服务器端口号};
}
2.3.2 connect - 向服务端发起连接
语法
#include sys/types.h /* See NOTES */
#include sys/socket.hint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明
int sockdfsocket文件描述符const struct sockaddr *addr传入参数指定服务器端地址信息含IP地址和端口号socklen_t addrlen传入参数传入sizeof(addr)大小返回值成功为 0失败为 -1。
代码呈现
// 启动客户端
void StartClient()
{// 填充服务器的 sockaddr_in 结构体信息struct sockaddr_in server;socklen_t len sizeof(server);memset(server, 0, len);server.sin_family AF_INET;inet_aton(server_ip_.c_str(), server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法server.sin_port htons(server_port_);// 尝试重连 5 次int n 5;while(n){int ret connect(sock_, (const struct sockaddr*)server, len);if(ret 0){// 连接成功可以跳出循环break;}// 尝试进行重连std::cerr 网络异常正在进行重连... 剩余连接次数: --n std::endl;sleep(1);}// 如果剩余重连次数为 0证明连接失败if(n 0){std::cerr 连接失败! strerror(errno) std::endl;close(sock_);exit(CONNECT_ERR);}// 连接成功std::cout 连接成功! std::endl;// 进行业务处理// GetService();
}
2.3.3 write、read - 向服务器发送数据、从服务器接收数据
和服务端一样客户端也是通过 write 和 read 接口来发送数据和读取数据。
2.3.4 代码呈现 client.hpp代码 #pragma once#include iostream
#include string
#include cstring
#include cerrno
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#includeunistd.h
#includesys/wait.hnamespace nt_client
{enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR,CONNECT_ERR};class TcpClient{public:TcpClient(const std::string ip, const uint16_t port):server_ip_(ip), server_port_(port){}~TcpClient(){}// 初始化客户端void InitClient(){// 创建套接字sock_ socket(AF_INET, SOCK_STREAM, 0);if (sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Sock Succeess! sock_ std::endl;}// 启动客户端void StartClient(){// 填充服务器的 sockaddr_in 结构体信息struct sockaddr_in server;socklen_t len sizeof(server);memset(server, 0, len);//清空server.sin_family AF_INET;//网络通信inet_aton(server_ip_.c_str(), server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法server.sin_port htons(server_port_);// 尝试重连 5 次int n 5;while (n){int ret connect(sock_, (const struct sockaddr *)server, len);if (ret 0){// 连接成功可以跳出循环break;}// 尝试进行重连std::cerr 网络异常正在进行重连... 剩余连接次数: --n std::endl;sleep(1);}// 如果剩余重连次数为 0证明连接失败if (n 0){std::cerr 连接失败! strerror(errno) std::endl;close(sock_);exit(CONNECT_ERR);}// 连接成功std::cout 连接成功! std::endl;// 获取服务GetService();}// 获取服务void GetService(){char buff[1024];std::string who server_ip_ - std::to_string(server_port_);while (true){// 由用户输入信息std::string msg;std::cout Please Enter ;std::getline(std::cin, msg);// 发送信息给服务器write(sock_, msg.c_str(), msg.size());// 接收来自服务器的信息ssize_t n read(sock_, buff, sizeof(buff) - 1);if (n 0){// 正常通信buff[n] \0;std::cout Client get: buff from who std::endl;}else if (n 0){// 读取到文件末尾服务器关闭了std::cout Server who quit! std::endl;close(sock_); // 关闭文件描述符break;}else{// 读取异常std::cerr Read Fail! strerror(errno) std::endl;close(sock_); // 关闭文件描述符break;}}}private:int sock_; // 套接字std::string server_ip_; // 服务器IPuint16_t server_port_; // 服务器端口号};
} client.cc代码 #include iostream
#include memory
#include client.hppusing namespace std;
using namespace nt_client;void Usage(const char* program)
{cout Usage: endl;cout \t program ServerIP ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 3){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型std::string ipargv[1];uint16_t port stoi(argv[2]);//将字符串转换成端口号unique_ptrTcpClient usvr (new TcpClient(ip,port));usvr-InitClient();usvr-StartClient();return 0;
}
2.4 多进程版服务器 2.4.1 父进程阻塞等待
功能阐述
当服务器成功处理连接请求后fork 新建一个子进程用于进行业务处理原来的进程专注于处理连接请求子进程创建成功后会继承父进程的文件描述符表能轻而易举的获取客户端的 socket 套接字从而进行网络通信当然不止文件描述符表得益于 写时拷贝 机制子进程还会共享父进程的变量当发生修改行为时才会自己创建。
代码呈现
// 进程创建、等待所需要的头文件
#include unistd.h
#include sys/wait.h
#include sys/types.h// 启动服务器void StartServer(){for (;;){// 1.获取新连接struct sockaddr_in client;socklen_t len sizeof(client);int accept_socket accept(listen_sock_, (struct sockaddr *)client, len);if (accept_socket 0){std::cout accept failed std::endl;continue;}// 2.业务处理// 2.1客户端信息存储uint16_t clientport ntohs(client.sin_port); // 客户端端口号char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip)); // 客户端IPstd::cout Server accept clientip - clientport accept_socket from listen_sock_ success! std::endl;// 3.创建子进程pid_t id fork();if (id 0){// 创建子进程失败暂时不与当前客户端建立通信会话close(accept_socket);std::cerr Fork Fail! std::endl;}else if (id 0){// 子进程内close(listen_sock_); // 子进程不需要监听建议关闭// 执行业务处理函数Service(accept_socket, clientip, clientport);exit(0); // 子进程退出}else//父进程{// 父进程需要等待子进程pid_t ret waitpid(id, nullptr, 0); // 默认为阻塞式等待if (ret id)std::cout Wait id success!;}}}
细节说明
虽然此时成功创建了子进程但父进程处理连接请求仍然需要等待子进程退出后才能继续运行说白了就是 父进程现在处于阻塞等待状态因此父进程应该需要设置为 非阻塞等待。
2.4.2 非阻塞等待版本
想让父进程不阻塞等待方法如下
通过参数设置为非阻塞等待不推荐设置 SIGCHLD 信号的处理动作为子进程回收不是很推荐忽略 SIGCHLD 信号推荐使用设置孙子进程不是很推荐
2.4.3 代码呈现
#pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#include signal.h // 信号处理相关头文件namespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog 5; // 请求队列的最大长度using func_t std::functionstd::string(std::string); // 回调函数类型enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer{public:TcpServer(const func_t func, const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), func_(func) // 注意这里要传1个业务处理函数{}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if (bind(listen_sock_, (const sockaddr *)local, sizeof(local)) 0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if (listen(listen_sock_, backlog) 0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);for (;;){// 1.获取新连接struct sockaddr_in client;socklen_t len sizeof(client);int accept_socket accept(listen_sock_, (struct sockaddr *)client, len);if (accept_socket 0){std::cout accept failed std::endl;continue;}// 2.业务处理// 2.1客户端信息存储uint16_t clientport ntohs(client.sin_port); // 客户端端口号char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip)); // 客户端IPstd::cout Server accept clientip - clientport accept_socket from listen_sock_ success! std::endl;// 3.创建子进程pid_t id fork();if (id 0){// 创建子进程失败暂时不与当前客户端建立通信会话close(accept_socket);std::cerr Fork Fail! std::endl;}else if (id 0){// 子进程内close(listen_sock_); // 子进程不需要监听建议关闭// 执行业务处理函数Service(accept_socket, clientip, clientport);exit(0); // 子进程退出}close(accept_socket); // 父进程不再需要资源建议关闭}}// 通信服务业务处理void Service(int sock, const std::string clientip, const uint16_t clientport){char buff[1024];std::string who clientip - std::to_string(clientport);while (true){ssize_t n read(sock, buff, sizeof(buff) - 1); // 预留 \0 的位置if (n 0){// 读取成功buff[n] \0;std::cout Server get: buff from who std::endl;std::string respond func_(buff); // 实际业务处理由上层指定// 发送给服务器write(sock, buff, strlen(buff));}else if (n 0){// 表示当前读取到文件末尾了结束读取std::cout Client who sock quit! std::endl;close(sock); // 关闭文件描述符break;}else{// 读取出问题暂时std::cerr Read Fail! strerror(errno) std::endl;close(sock); // 关闭文件描述符break;}}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址func_t func_; // 业务处理函数};
}
2.5 多线程版本服务器
从内核的观点看
进程的目的就是担当分配系统资源CPU时间、内存等的基本单位。线程是进程的一个执行流是CPU调度和分派的基本单位它是比进程更小的能独立运行的基本单位。
2.5.1 使用原生线程库
概念
由于我们创建线程是用来提供服务的而服务端的业务中有一个Service()它需要我们的线程去传入 Service() 函数中的所有参数同时也需要具备调用 Service() 业务处理函数的能力我们只能把Service() 函数中的所有参数和this指针传进去而这单凭一个 void* 的参数是无法解决的为此我们可以创建一个类里面可以包含我们所需要的参数——Service() 函数中的所有参数和this指针。
分析
所以接下来我们需要在连接成功后创建次线程利用已有信息构建 ThreadData 对象为次线程编写回调函数最终目的是为了执行 Service() 函数。
代码呈现 server.hpp代码 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#include signal.h // 信号处理相关头文件
#includepthread.hnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog 5; // 请求队列的最大长度enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer; // 前置声明// 包含我们所需参数的类型class ThreadData{public:ThreadData(int sock, const std::string ip, const uint16_t port, TcpServer* ptr):sock_(sock), clientip_(ip), clientport_(port), current_(ptr){}// 设置为公有是为了方便访问public:int sock_;std::string clientip_;uint16_t clientport_;TcpServer* current_; // 指向 TcpServer 对象的指针};using func_t std::functionstd::string(std::string); // 回调函数类型class TcpServer{public:TcpServer(const func_t func, const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), func_(func) // 注意这里要传1个业务处理函数{}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if (bind(listen_sock_, (const sockaddr *)local, sizeof(local)) 0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if (listen(listen_sock_, backlog) 0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){// 忽略 SIGCHLD 信号signal(SIGCHLD, SIG_IGN);for (;;){// 1.获取新连接struct sockaddr_in client;socklen_t len sizeof(client);int accept_socket accept(listen_sock_, (struct sockaddr *)client, len);if (accept_socket 0){std::cout accept failed std::endl;continue;}// 2.业务处理// 2.1客户端信息存储uint16_t clientport ntohs(client.sin_port); // 客户端端口号char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip)); // 客户端IPstd::cout Server accept clientip - clientport accept_socket from listen_sock_ success! std::endl;// 3.创建线程及所需要的线程信息类ThreadData* td new ThreadData(accept_socket, clientip, clientport, this);pthread_t p;pthread_create(p, nullptr, Routine, td);}}// 线程回调函数static void* Routine(void* args){// 线程分离pthread_detach(pthread_self());ThreadData* td static_castThreadData*(args);// 调用业务处理函数td-current_-Service(td-sock_, td-clientip_, td-clientport_);// 销毁对象delete td;return (void*)0;}// 通信服务业务处理void Service(int sock, const std::string clientip, const uint16_t clientport){char buff[1024];std::string who clientip - std::to_string(clientport);while (true){ssize_t n read(sock, buff, sizeof(buff) - 1); // 预留 \0 的位置if (n 0){// 读取成功buff[n] \0;std::cout Server get: buff from who std::endl;std::string respond func_(buff); // 实际业务处理由上层指定// 发送给服务器write(sock, buff, strlen(buff));}else if (n 0){// 表示当前读取到文件末尾了结束读取std::cout Client who sock quit! std::endl;close(sock); // 关闭文件描述符break;}else{// 读取出问题暂时std::cerr Read Fail! strerror(errno) std::endl;close(sock); // 关闭文件描述符break;}}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址func_t func_; // 业务处理函数};
} makefile代码呈现 .PHONY:all
all:server clientserver:server.ccg -o $ $^ -stdc11 -lpthreadclient:client.ccg -o $ $^ -stdc11 -lpthread.PHONY:clean
clean:rm -rf server client
2.5.2 线程池版本
问题分析
如果每来一个用户我们就得创建一个线程那么当来了很多用户就会消耗很多资源。我们不想等到客户来了才创建我们的线程我们可以提前创建好我们不提供死循环服务为此可以改用之前实现的线程池。
代码呈现 ThreadPool.hpp代码 #pragma once#include iostream
#include vector
#include string
#include queue
#include pthread.h
#include unistd.h// 线程信息结构体
struct ThreadInfo
{pthread_t tid; // 线程IDstd::string name; // 线程名称
};// 默认线程数量
static const int defalutnum 5;// 线程池模板类
template class T
class ThreadPool
{
private:// 互斥锁加锁函数void Lock(){pthread_mutex_lock(mutex_);}// 互斥锁解锁函数void Unlock(){pthread_mutex_unlock(mutex_);}// 唤醒等待的线程void Wakeup(){pthread_cond_signal(cond_);}// 线程休眠等待条件变量void ThreadSleep(){pthread_cond_wait(cond_, mutex_);}// 判断任务队列是否为空bool IsQueueEmpty(){return tasks_.empty();}// 根据线程ID获取线程名称std::string GetThreadName(pthread_t tid){for (const auto ti : threads_){if (ti.tid tid)return ti.name;}return None;}public:// 线程处理任务的函数static void *HandlerTask(void *args){ThreadPoolT *tp static_castThreadPoolT *(args);std::string name tp-GetThreadName(pthread_self());while (true){tp-Lock();while (tp-IsQueueEmpty()){tp-ThreadSleep();}T t tp-Pop();tp-Unlock();t.Run();}}// 启动线程池中的所有线程void Start(){int num threads_.size();for (int i 0; i num; i){threads_[i].name thread- std::to_string(i 1);pthread_create((threads_[i].tid), nullptr, HandlerTask, this);}}// 从任务队列中取出一个任务T Pop(){T t tasks_.front();tasks_.pop();return t;}// 向任务队列中添加一个任务void Push(const T t){Lock();tasks_.push(t);Wakeup();Unlock();}// 获取线程池单例对象static ThreadPoolT *GetInstance(){if (nullptr tp_) // 如果线程池对象不存在则创建一个新的线程池对象{pthread_mutex_lock(lock_); // 加锁保证线程安全if (nullptr tp_) // 再次检查是否已经创建了线程池对象防止多线程环境下的竞争条件{std::cout log: singleton create done first! std::endl;tp_ new ThreadPoolT(); // 创建线程池对象}pthread_mutex_unlock(lock_); // 解锁}return tp_; // 返回线程池对象指针}private:// 构造函数初始化线程池可以指定线程数量默认为defalutnumThreadPool(int num defalutnum) : threads_(num){pthread_mutex_init(mutex_, nullptr); // 初始化互斥锁pthread_cond_init(cond_, nullptr); // 初始化条件变量}// 析构函数销毁线程池资源~ThreadPool(){pthread_mutex_destroy(mutex_); // 销毁互斥锁pthread_cond_destroy(cond_); // 销毁条件变量}// 禁止拷贝构造和赋值操作符确保线程池对象的单一性ThreadPool(const ThreadPoolT ) delete;const ThreadPoolT operator(const ThreadPoolT ) delete; // abc
private:// 线程信息列表std::vectorThreadInfo threads_;// 任务队列std::queueT tasks_;// 互斥锁和条件变量用于同步和通信pthread_mutex_t mutex_;pthread_cond_t cond_;// 线程池单例对象指针和互斥锁静态成员变量static ThreadPoolT *tp_;static pthread_mutex_t lock_;
};// 初始化线程池单例对象指针和互斥锁静态成员变量
template class T
ThreadPoolT *ThreadPoolT::tp_ nullptr;
template class T
pthread_mutex_t ThreadPoolT::lock_ PTHREAD_MUTEX_INITIALIZER; Task.hpp代码 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#includepthread.husing func_t std::functionstd::string(std::string); // 回调函数类型// 任务类
class Task
{
public:Task(int sockfd, const std::string clientip, const uint16_t clientport,const func_t func): sockfd_(sockfd), clientip_(clientip), clientport_(clientport), func_2(func) // 注意这里要传1个业务处理函数{}~Task(){}void Run(){char buff[1024];std::string who clientip_ - std::to_string(clientport_);//while (true),如果线程池的线程一直循环在干这件事的话效率会极其低下ssize_t n read((sockfd_),buff, sizeof(buff)- 1); // 预留 \0 的位置if (n 0){// 读取成功buff[n] \0;std::cout Server get: buff from who std::endl;std::string respond func_2(buff); // 实际业务处理由上层指定// 发送给服务器write(sockfd_, buff, strlen(buff));}else if (n 0){// 表示当前读取到文件末尾了结束读取std::cout Client who sockfd_ quit! std::endl;close(sockfd_); // 关闭文件描述符 }else{// 读取出问题暂时std::cerr Read Fail! strerror(errno) std::endl;close(sockfd_); // 关闭文件描述符}}private:int sockfd_;//accept返回的套接字std::string clientip_;//用户IPuint16_t clientport_;//用户端口号func_t func_2; // 业务处理函数
}; server.hpp代码 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#includepthread.h
#includeThreadpool.hpp
#includeTask.hppnamespace nt_server
{const uint16_t default_port 8877; // 默认端口号const std::string default_ip 0.0.0.0; // 默认IPconst int backlog 5; // 请求队列的最大长度enum{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR};class TcpServer; // 前置声明// 包含我们所需参数的类型class ThreadData{public:ThreadData(int sock, const std::string ip, const uint16_t port, TcpServer* ptr):sock_(sock), clientip_(ip), clientport_(port), current_(ptr){}// 设置为公有是为了方便访问public:int sock_;std::string clientip_;uint16_t clientport_;TcpServer* current_; // 指向 TcpServer 对象的指针};using func_t std::functionstd::string(std::string); // 回调函数类型class TcpServer{public:TcpServer(const func_t func, const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), func_1(func) // 注意这里要传1个业务处理函数{}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if (bind(listen_sock_, (const sockaddr *)local, sizeof(local)) 0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if (listen(listen_sock_, backlog) 0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){for (;;){//线程池的设计--》使用之前必须先启动ThreadPoolTask::GetInstance()-Start();// 1.获取新连接struct sockaddr_in client;socklen_t len sizeof(client);int accept_socket accept(listen_sock_, (struct sockaddr *)client, len);if (accept_socket 0){std::cout accept failed std::endl;continue;}// 2.业务处理// 2.1客户端信息存储uint16_t clientport ntohs(client.sin_port); // 客户端端口号char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip)); // 客户端IPstd::cout Server accept clientip - clientport accept_socket from listen_sock_ success! std::endl;// 3.把任务交给线程池Task task1(accept_socket,clientip,clientport,func_1);//和原来Server函数的参数差不多这里还多传了一个回调函数ThreadPoolTask::GetInstance()-Push(task1);}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址func_t func_1; // 业务处理函数};
} server.cc代码呈现 #include string
#include vector
#include memory // 智能指针相关头文件
#include cstdio
#include server.hppusing namespace std;
using namespace nt_server;// 业务处理回调函数字符串回响
string echo(string request)
{return request;
}void Usage(const char* program)
{cout Usage: endl;cout \t program ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrTcpServer usvr (new TcpServer(echo,port));usvr-InitServer();usvr-StartServer();return 0;
}
2.6 守护进程版服务器 2.6.1 守护进程 Daemon
概念
守护进程是一种长期运行的进程守护进程的生存期不一定长但一般应该这样做一般是操作系统启动的时候它就启动操作系统关闭的时候它才关闭。守护进程跟终端无关联也就是说它们没有控制终端所以控制终端退出也不会导致守护进程退出。守护进程是在后台运行的不会占着终端终端可以执行其他命令。
分析
守护进程是运行在后台的一种特殊进程它独立于控制终端并且周期性地执行某种任务或循环等待处理某些事件的发生它不需要用户输入就能运行而且提供某种服务不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的。守护进程一般在系统启动时开始运行除非强行终止否则直到系统关机才随之一起停止运行。守护进程一般都以root用户权限运行因为要使用某些特殊的端口1-1024或者资源。守护进程的父进程一般都是init进程因为它真正的父进程在fork出守护进程后就直接退出了所以守护进程都是孤儿进程由init接管。守护进程是非交互式程序没有控制终端所以任何输出无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。守护进程的名称通常以d结尾比如sshd、xinetd、crond等。
细节说明
守护进程是一个生存周期较长的进程通常独立于控制终端并且周期性的执行某种任务或者等待处理某些待发生的事件。大多数服务都是通过守护进程实现的。关闭终端相应的进程都会被关闭而守护进程却能够突破这种限制。
总结
守护进程不会收到来自内核的 SIGHUP 信号也就是说如果守护进程收到了 SIGHUP 信号那么肯定是另外的进程发的。守护进程把 SIGHUP 信号作为通知信号表示配置文件已经发生改动守护进程应该重新读入其配置文件。守护进程不会收到来自内核的 SIGINT 信号CtrlC)、SIGWINCH 信号终端窗口大小改变。
2.6.2 进程组
什么是进程组
进程组就是一个或多个进程的集合。这些进程并不是孤立的他们彼此之间或者存在父子、兄弟关系或者在功能上有相近的联系。每个进程都有父进程而所有的进程以init进程为根形成一个树状结构。
Linux为什么要有进程组
提供进程组就是为了方便对进程进行管理。假设要完成一个任务需要同时并发100个进程。当用户处于某种原因要终止 这个任务时要是没有进程组就需要手动的一个个去杀死这100个进程并且必须要严格按照进程间父子兄弟关系顺序否则会扰乱进程树。
修改进程组ID的接口
int setpgid(pid_t pid, pid_t pgid);
这个函数的含义是找到进程ID为pid的进程将这个进程的进程组ID修改为pgid如果pid的值为0则表示要修改调用进程的进程组ID。该接口一般用来创建一个新的进程组。
2.6.3 作业
概念
Shell分前后台来控制的不是进程而是作业Job或者进程组Process Group。一个前台作业可以由多个进程组成一个后台也可以由多个进程组成Shell可以运行一个前台作业和任意多个后台作业这称为作业控制。
作业与进程组的区别
如果作业中的某个进程又创建了子进程则子进程不属于作业。一旦作业运行结束Shell就把自己提到前台如果原来的前台进程还存在如果这个子进程还没终止它自动变为后台进程组。一个或多个进程组的集合比如用户从登陆到退出这个期间用户运行的所有进程都属于该会话周期。
2.6.4 会话
概念
由于Linux是多用户多任务的分时系统所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话 。一个会话可包含多个进程组但只能有一个前台进程组。每个会话都有一个会话首领leader即创建会话的进程。 sys_setsid()调用能创建一个会话。
语法
#include unistd.h
pid_t setsid(void);
如果这个函数的调用进程不是进程组组长那么调用该函数会发生以下事情
创建一个新会话会话ID等于进程ID调用进程成为会话的首进程。创建一个进程组进程组ID等于进程ID调用进程成为进程组的组长。该进程没有控制终端如果调用setsid前该进程有控制终端这种联系就会断掉。
2.6.5 控制终端
概念
与控制终端建立连接的会话领头进程称为控制进程 (session leader) 一个会话可以有一个控制终端。这通常是登陆到其上的终端设备在终端登陆情况下或伪终端设备在网络登陆情况下。建立与控制终端连接的会话首进程被称为控制进程。
总结
进程属于一个进程组进程组属于一个会话会话可能有也可能没有控制终端。一般而言当用户在某个终端上登录时一个新的会话就开始了。进程组由组中的领头进程标识领头进程的进程标识符就是进程组的组标识符。类似地每个会话也对应有一个领头进程。同一会话中的进程通过该会话的领头进程和一个终端相连该终端作为这个会话的控制终端。一个会话只能有一个控制终端而一个控制终端只能控制一个会话。用户通过控制终端可以向该控制终端所控制的会话中的进程发送键盘信号。同一会话中只能有一个前台进程组属于前台进程组的进程可从控制终端获得输入而其他进程均是后台进程可能分属于不同的后台进程组。
2.6.6 创建守护进程的过程
①fork()创建子进程父进程exit()退出
这是创建守护进程的第一步。由于守护进程是脱离控制终端的因此完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成而用户在Shell终端里则可以执行其他命令从而在形式上做到了与控制终端的脱离在后台工作。
②在子进程中调用 setsid() 函数创建新的会话
在调用了fork()函数后子进程全盘拷贝了父进程的会话期、进程组、控制终端等虽然父进程退出了但会话期、进程组、控制终端等并没有改变因此这还不是真正意义上的独立开来而 setsid() 函数能够使进程完全独立出来。
③再次 fork() 一个孙进程并让子进程退出
为什么要再次fork呢假定有这样一种情况之前的父进程fork出子进程以后还有别的事情要做在做事情的过程中因为某种原因阻塞了而此时的子进程因为某些非正常原因要退出的话就会形成僵尸进程所以由子进程fork出一个孙进程以后立即退出孙进程作为守护进程会被init接管此时无论父进程想做什么都随它了。
④在孙进程中调用 chdir() 函数让根目录 ”/” 成为孙进程的工作目录
这一步也是必要的步骤使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中当前目录所在的文件系统如“/mnt/usb”是不能卸载的这对以后的使用会造成诸多的麻烦比如系统由于某种原因要进入单用户模式。因此通常的做法是让/作为守护进程的当前工作目录这样就可以避免上述的问题当然如有特殊需要也可以把当前工作目录换成其他的路径如/tmp改变工作目录的常见函数是chdir。
⑤在孙进程中调用 umask() 函数设置进程的文件权限掩码为0
文件权限掩码是指屏蔽掉文件权限中的对应位。比如有个文件权限掩码是050它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码这就给该子进程使用文件带来了诸多的麻烦。因此把文件权限掩码设置为0可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里通常的使用方法为umask(0)。
⑥在孙进程中关闭任何不需要的文件描述符
同文件权限码一样用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写但它们一样消耗系统资源而且可能导致所在的文件系统无法卸下。在上面的第2)步之后守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程守护进程中用常规方法如printf输出的字符也不可能在终端上显示出来。所以文件描述符为0、1和2 的3个文件常说的输入、输出和报错已经失去了存在的价值也应被关闭。
⑦守护进程退出处理
当用户需要外部停止守护进程运行时往往会使用 kill 命令停止该守护进程。所以守护进程中需要编码来实现 kill 发出的signal信号处理达到进程的正常退出。
2.6.7 直接调用系统现成的接口
语法
#include unistd.hint daemon(int nochdir, int noclose);
参数说明
nochdir如果该参数为0则将当前工作目录更改为根目录如果为1则不更改当前工作目录。noclose如果该参数为0则关闭所有与终端相关的文件描述符如果为1则不关闭文件描述符。
server.cc 服务器源文件
#include string
#include vector
#include memory // 智能指针相关头文件
#include cstdio
#include server.hppusing namespace std;
using namespace nt_server;// 业务处理回调函数字符串回响
string echo(string request)
{return request;
}void Usage(const char* program)
{cout Usage: endl;cout \t program ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}// 直接守护进程化daemon(0, 0);//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrTcpServer usvr (new TcpServer(echo,port));usvr-InitServer();usvr-StartServer();return 0;
}
2.6.8 自己创建守护进程版本的服务器
手动实现守护进程时需要注意以下几点
忽略异常信号0、1、2 要做特殊处理文件描述符进程的工作路径可能要改变从用户目录中脱离至根目录
具体实现步骤如下
忽略常见的异常信号SIGPIPE、SIGCHLD。如何保证自己不是组长 创建子进程 成功后父进程退出子进程变成守护进程。新建会话自己成为会话的 话首进程。可选更改守护进程的工作路径chdir。处理后续对于 0、1、2 的问题。
对于标准输入、标准输出、标准错误 的处理方式有两种
暴力处理直接关闭 fd优雅处理将 fd 重定向至 /dev/null也就是 daemon() 函数的做法
2.6.9 代码呈现 Daemon.hpp 守护进程头文件 #pragma once#include iostream
#include cstring
#include cerrno
#include signal.h
#include unistd.h
#include sys/types.h
#include sys/stat.h
#include fcntl.henum
{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LIS_ERR,CONNECT_ERR,FORK_ERR,SETSID_ERR,CHDIR_ERR,OPEN_ERR
};
//因为这个头文件会被server.hpp包含server.hpp会被server.cc包含
//刚好这三个文件里都要使用这些退出码信息所以放在这里一次即可static const char *path /home/zs_108/A;//设置守护进程的工作目录这里大家要自己设置啊void Daemon()
{// 1、忽略常见信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2、创建子进程自己退休pid_t id fork();if (id 0)exit(0);else if (id 0){// 子进程创建失败std::coutFork Fail: strerror(errno)std::endl;exit(FORK_ERR);}// 3、新建会话使自己成为一个单独的组pid_t ret setsid();if (ret -1){// 守护化失败std::coutSetsid Fail: strerror(errno)std::endl;exit(SETSID_ERR);}// 4、更改工作路径int n chdir(path);if (n -1){// 更改路径失败std::coutChdir Fail: strerror(errno)std::endl;exit(CHDIR_ERR);}// 5、重定向标准输入输出错误int fd open(/dev/null, O_RDWR);if (fd -1){// 文件打开失败std::coutOpen Fail: strerror(errno)std::endl;exit(OPEN_ERR);}// 重定向标准输入、标准输出、标准错误dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);
} Task.hpp代码 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#includepthread.husing func_t std::functionstd::string(std::string); // 回调函数类型// 任务类
class Task
{
public:Task(int sockfd, const std::string clientip, const uint16_t clientport,const func_t func): sockfd_(sockfd), clientip_(clientip), clientport_(clientport), func_2(func) // 注意这里要传1个业务处理函数{}~Task(){}void Run(){char buff[1024];std::string who clientip_ - std::to_string(clientport_);//while (true),如果线程池的线程一直循环在干这件事的话效率会极其低下ssize_t n read((sockfd_),buff, sizeof(buff)- 1); // 预留 \0 的位置if (n 0){// 读取成功buff[n] \0;std::cout Server get: buff from who std::endl;std::string respond func_2(buff); // 实际业务处理由上层指定// 发送给服务器write(sockfd_, buff, strlen(buff));}else if (n 0){// 表示当前读取到文件末尾了结束读取std::cout Client who sockfd_ quit! std::endl;close(sockfd_); // 关闭文件描述符 }else{// 读取出问题暂时std::cerr Read Fail! strerror(errno) std::endl;close(sockfd_); // 关闭文件描述符}close(sockfd_);//由于没有死循环我们必须将其关闭}private:int sockfd_;//accept返回的套接字std::string clientip_;//用户IPuint16_t clientport_;//用户端口号func_t func_2; // 业务处理函数
}; ThreadPool.hpp代码 #pragma once#include iostream
#include vector
#include string
#include queue
#include pthread.h
#include unistd.h// 线程信息结构体
struct ThreadInfo
{pthread_t tid; // 线程IDstd::string name; // 线程名称
};// 默认线程数量
static const int defalutnum 5;// 线程池模板类
template class T
class ThreadPool
{
private:// 互斥锁加锁函数void Lock(){pthread_mutex_lock(mutex_);}// 互斥锁解锁函数void Unlock(){pthread_mutex_unlock(mutex_);}// 唤醒等待的线程void Wakeup(){pthread_cond_signal(cond_);}// 线程休眠等待条件变量void ThreadSleep(){pthread_cond_wait(cond_, mutex_);}// 判断任务队列是否为空bool IsQueueEmpty(){return tasks_.empty();}// 根据线程ID获取线程名称std::string GetThreadName(pthread_t tid){for (const auto ti : threads_){if (ti.tid tid)return ti.name;}return None;}public:// 线程处理任务的函数static void *HandlerTask(void *args){ThreadPoolT *tp static_castThreadPoolT *(args);std::string name tp-GetThreadName(pthread_self());while (true){tp-Lock();while (tp-IsQueueEmpty()){tp-ThreadSleep();}T t tp-Pop();tp-Unlock();t.Run();}}// 启动线程池中的所有线程void Start(){int num threads_.size();for (int i 0; i num; i){threads_[i].name thread- std::to_string(i 1);pthread_create((threads_[i].tid), nullptr, HandlerTask, this);}}// 从任务队列中取出一个任务T Pop(){T t tasks_.front();tasks_.pop();return t;}// 向任务队列中添加一个任务void Push(const T t){Lock();tasks_.push(t);Wakeup();Unlock();}// 获取线程池单例对象static ThreadPoolT *GetInstance(){if (nullptr tp_) // 如果线程池对象不存在则创建一个新的线程池对象{pthread_mutex_lock(lock_); // 加锁保证线程安全if (nullptr tp_) // 再次检查是否已经创建了线程池对象防止多线程环境下的竞争条件{std::cout log: singleton create done first! std::endl;tp_ new ThreadPoolT(); // 创建线程池对象}pthread_mutex_unlock(lock_); // 解锁}return tp_; // 返回线程池对象指针}private:// 构造函数初始化线程池可以指定线程数量默认为defalutnumThreadPool(int num defalutnum) : threads_(num){pthread_mutex_init(mutex_, nullptr); // 初始化互斥锁pthread_cond_init(cond_, nullptr); // 初始化条件变量}// 析构函数销毁线程池资源~ThreadPool(){pthread_mutex_destroy(mutex_); // 销毁互斥锁pthread_cond_destroy(cond_); // 销毁条件变量}// 禁止拷贝构造和赋值操作符确保线程池对象的单一性ThreadPool(const ThreadPoolT ) delete;const ThreadPoolT operator(const ThreadPoolT ) delete; // abc
private:// 线程信息列表std::vectorThreadInfo threads_;// 任务队列std::queueT tasks_;// 互斥锁和条件变量用于同步和通信pthread_mutex_t mutex_;pthread_cond_t cond_;// 线程池单例对象指针和互斥锁静态成员变量static ThreadPoolT *tp_;static pthread_mutex_t lock_;
};// 初始化线程池单例对象指针和互斥锁静态成员变量
template class T
ThreadPoolT *ThreadPoolT::tp_ nullptr;
template class T
pthread_mutex_t ThreadPoolT::lock_ PTHREAD_MUTEX_INITIALIZER; server.hpp代码 #pragma once#include iostream
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include cstring
#include unistd.h
#include functional
#include unistd.h
#include sys/wait.h
#include pthread.h
#include Threadpool.hpp
#include Task.hpp
#include Daemon.hppconst uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IP
const int backlog 5; // 请求队列的最大长度class TcpServer; // 前置声明// 包含我们所需参数的类型
class ThreadData
{
public:ThreadData(int sock, const std::string ip, const uint16_t port, TcpServer *ptr): sock_(sock), clientip_(ip), clientport_(port), current_(ptr){}// 设置为公有是为了方便访问
public:int sock_;std::string clientip_;uint16_t clientport_;TcpServer *current_; // 指向 TcpServer 对象的指针
};using func_t std::functionstd::string(std::string); // 回调函数类型class TcpServer
{
public:TcpServer(const func_t func, const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port), func_1(func) // 注意这里要传1个业务处理函数{}~TcpServer(){}// 初始化服务器void InitServer(){// 1.创建套接字listen_sock_ socket(AF_INET, SOCK_STREAM, 0);if (listen_sock_ -1){std::cerr Create Socket Fail! strerror(errno) std::endl;exit(SOCKET_ERR);}std::cout Create Socket Success! listen_sock_ std::endl;// 2.绑定IP地址与端口号struct sockaddr_in local;memset(local, 0, sizeof(local)); // 清零local.sin_family AF_INET; // 网络local.sin_addr.s_addr inet_addr(default_ip.c_str()); // 我设置为默认绑定任意可用IP地址local.sin_port htons(port_); // 我设置为默认是8877if (bind(listen_sock_, (const sockaddr *)local, sizeof(local)) 0){std::cout Bind IPPort Fail: strerror(errno) std::endl;exit(BIND_ERR);}if (listen(listen_sock_, backlog) 0){perror(Listen fail);exit(LIS_ERR);}}// 启动服务器void StartServer(){// 守护进程化Daemon();for (;;){// 线程池的设计--》使用之前必须先启动ThreadPoolTask::GetInstance()-Start();// 1.获取新连接struct sockaddr_in client;socklen_t len sizeof(client);int accept_socket accept(listen_sock_, (struct sockaddr *)client, len);if (accept_socket 0){std::cout accept failed std::endl;continue;}// 2.业务处理// 2.1客户端信息存储uint16_t clientport ntohs(client.sin_port); // 客户端端口号char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip)); // 客户端IPstd::cout Server accept clientip - clientport accept_socket from listen_sock_ success! std::endl;// 3.把任务交给线程池Task task1(accept_socket, clientip, clientport, func_1); // 和原来Server函数的参数差不多这里还多传了一个回调函数ThreadPoolTask::GetInstance()-Push(task1);}}private:int listen_sock_; // socket套接字uint16_t port_; // 端口号std::string ip_; // ip地址func_t func_1; // 业务处理函数
}; server.cc代码 #include string
#include vector
#include memory // 智能指针相关头文件
#include cstdio
#include server.hppusing namespace std;// 业务处理回调函数字符串回响
string echo(string request)
{return request;
}void Usage(const char* program)
{cout Usage: endl;cout \t program ServerPort endl;
}int main(int argc, char* argv[])
{if (argc ! 2){// 错误的启动方式提示错误信息Usage(argv[0]);return USAGE_ERR;}//命令行参数都是字符串我们需要将其转换成对应的类型uint16_t port stoi(argv[1]);//将字符串转换成端口号unique_ptrTcpServer usvr (new TcpServer(echo,port));usvr-InitServer();usvr-StartServer();return 0;
}
2.7 问题剖析 2.7.1实际连接过程没有这么简单
解释
真正连接的过程实际就是双方操作系统三次握手的过程这个过程是由双方的操作系统自动完成的。 我们知道上层发起连接请求和收获连接结果是通过connect和accept系统调用来完成的而真实的连接过程和这两个系统调用没什么关系连接过程是由双方的操作系统执行各自的内核代码自动完成连接过程的。 所以accept并不参与三次握手的任何细节他仅仅只负责拿走连接结果的胜利果实。换句话说就算上层不调用accept三次握手的过程也能够建立好因为应用是应用底层是底层三次握手就是底层和你应用没半毛钱关系这是我双方的操作系统自主完成的工作。
另外我们所说的TCP协议保证可靠性和应用有关系吗照样没半毛钱关系因为应用是应用底层是底层TCP协议是传输层的传输层在操作系统内部实现。 2.7.2 维护TCP的连接有成本嘛
答案有
一定是有的因为双方的操作系统要在各自底层建立描述连接的结构对象然后用数据结构将这些结构对象管理起来这些都是要花时间和内存空间的所以维护连接一定是有成本的。
2.7.3 简单理解三次握手和四次挥手
理解三次握手和四次挥手
在链接过程中tcp采用三次握手。在断线过程中tcp采用四次挥手。
三次握手
client调用connect向服务器发起连接请求connect会发出SYN段并阻塞等待服务器应答(第一次)服务器收到客户端的SYN段后会给客户端应答一个SYN-ACK段表示同意建立连接(第二次)客户端收到SYN-ACK段后会从connect系统调用返回同时应答一个ACK段(第三次)此时连接建立成功。
四次握手
客户端如果没有请求之后就会调用close关闭连接此时客户端会向服务器发送FIN段(第一次)服务器收到FIN段后会回应一个ACK段(第二次)同时服务器的read会读到0当read返回后服务器就会知道客户端关闭了连接他此时也会调用close关闭连接同时向客户端发送FIN段(第三次) 客户端收到FIN段后会给服务器返回一个ACK段(第四次)。 socketAPI的connect被调用时会发出SYN段read返回时表明服务器收到FIN段
2.7.4 TCP通信的实质
TCP通信的实质
这些全部都是由TCP协议自己决定的这是操作系统内部的事情和我们用户层没有任何瓜葛这也就是为什么TCP叫做传输控制协议的原因因为传输的过程是由他自己所控制决定的。c-s和s-c之间发送使用的是不同对的发送和接收缓冲区所以c给s发是不影响s给c发送的这也就能说明TCP是全双工的一个在发送时不影响另一个也再发送所以网络发送的本质就是数据拷贝。 应用层缓冲区是什么
其实所谓的应用层缓冲区就是我们自己定义的buffer可以看到下面的6个网络发送接收接口都有对应的buf形参我们在使用的时候肯定要传参数进去而传的参数就是我们在应用层所定义出来的缓冲区。
2.8 封装接口
Socket.hpp代码
#pragma once #include iostream
#include string
#include unistd.h
#include cstring
#include sys/types.h
#include sys/stat.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h // 定义一些错误代码
enum
{ SocketErr 2, // 套接字创建错误 BindErr, // 绑定错误 ListenErr, // 监听错误
}; // 监听队列的长度
const int backlog 10; class Sock //服务器专门使用
{
public: Sock() : sockfd_(-1) // 初始化时将sockfd_设为-1表示未初始化的套接字 { } ~Sock() { // 析构函数中可以关闭套接字但这里选择不在析构函数中关闭因为有时需要手动管理资源 } // 创建套接字 void Socket() { sockfd_ socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ 0) { printf(socket error, %s: %d, strerror(errno), errno); //错误 exit(SocketErr); // 发生错误时退出程序 } int opt1;setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,opt,sizeof(opt)); //关闭后快速重启} // 将套接字绑定到指定的端口上 void Bind(uint16_t port) { //让服务器绑定IP地址与端口号struct sockaddr_in local; memset(local, 0, sizeof(local));//清零 local.sin_family AF_INET; // 网络local.sin_port htons(port); // 我设置为默认绑定任意可用IP地址local.sin_addr.s_addr INADDR_ANY; // 监听所有可用的网络接口 if (bind(sockfd_, (struct sockaddr *)local, sizeof(local)) 0) //让自己绑定别人{ printf(bind error, %s: %d, strerror(errno), errno); exit(BindErr); } } // 监听端口上的连接请求 void Listen() { if (listen(sockfd_, backlog) 0) { printf(listen error, %s: %d, strerror(errno), errno); exit(ListenErr); } } // 接受一个连接请求 int Accept(std::string *clientip, uint16_t *clientport) { struct sockaddr_in peer; socklen_t len sizeof(peer); int newfd accept(sockfd_, (struct sockaddr*)peer, len); if(newfd 0) { printf(accept error, %s: %d, strerror(errno), errno); return -1; } char ipstr[64]; inet_ntop(AF_INET, peer.sin_addr, ipstr, sizeof(ipstr)); *clientip ipstr; *clientport ntohs(peer.sin_port); return newfd; // 返回新的套接字文件描述符 } // 连接到指定的IP和端口——客户端才会用的 bool Connect(const std::string ip, const uint16_t port) { struct sockaddr_in peer;//服务器的信息 memset(peer, 0, sizeof(peer)); peer.sin_family AF_INET; peer.sin_port htons(port);inet_pton(AF_INET, ip.c_str(), (peer.sin_addr)); int n connect(sockfd_, (struct sockaddr*)peer, sizeof(peer)); if(n -1) { std::cerr connect to ip : port error std::endl; return false; } return true; } // 关闭套接字 void Close() { close(sockfd_); } // 获取套接字的文件描述符 int Fd() { return sockfd_; } private: int sockfd_; // 套接字文件描述符
};
server.hpp代码
const uint16_t default_port 8877; // 默认端口号
const std::string default_ip 0.0.0.0; // 默认IPclass TcpServer
{
public:TcpServer(const uint16_t port default_port, const std::string ip default_ip): ip_(ip), port_(port){}~TcpServer(){}bool InitServer(){listensock_.Socket();listensock_.Bind(port_);listensock_.Listen(); }void Start(){while(true){std::string clientip;uint16_t clientport;int sockfdlistensock_.Accept(clientip,clientport);//这里会返回一个新的套接字if(socket0) continue;//提供服务if(fork()0){listensock_.Close();//通过sockfd使用提供服务std::string inbuf;while (1){char buf[1024];// 1.读取客户端发送的信息ssize_t s read(sockfd, buf, sizeof(buf) - 1);if (s 0){ // s 0代表对方发送了空消息视作客户端主动退出printf(client quit: %s[%d], clientip.c_str(), clientport);break;}else if (s 0){// 出现了读取错误打印错误后断开连接printf(read err: %s[%d] %s, clientip.c_str(), clientport, strerror(errno));break;}else // 2.读取成功{}}exit(0);//子进程退出}close(sockfd);//}}private:uint16_t port_;Sock listensock_;//专门用来listen的std::string ip_; // ip地址
}
三、结束语 今天内容就到这里啦时间过得很快大家沉下心来好好学习会有一定的收获的大家多多坚持嘻嘻成功路上注定孤独因为坚持的人不多。那请大家举起自己的小手给博主一键三连有你们的支持是我最大的动力回见。