济南网站制作专业,视频网站中滑动列表怎么做的,网站外链是什么意思,WordPress网站运行时间目录
一、TCP网络程序
1.1 服务端初始化
1.1.1 创建套接字
1.1.2 服务端绑定
1.1.3 服务端监听
1.2 服务端启动
1.2.1 服务端获取连接
1.2.2 服务端处理请求
1.3 客户端初始化
1.4 客户端启动
1.4.1 发起连接
1.4.2 发起请求
1.5 网络测试
1.6 单执行流服务端的…目录
一、TCP网络程序
1.1 服务端初始化
1.1.1 创建套接字
1.1.2 服务端绑定
1.1.3 服务端监听
1.2 服务端启动
1.2.1 服务端获取连接
1.2.2 服务端处理请求
1.3 客户端初始化
1.4 客户端启动
1.4.1 发起连接
1.4.2 发起请求
1.5 网络测试
1.6 单执行流服务端的弊端
二、多进程版TCP网络程序
2.1 存在问题
2.2 捕捉SIGCHLD信号
2.3 孙子进程提供服务
三、多线程版TCP网络程序
四、线程池版TCP网络程序
五、地址转换函数
5.1 字符串IP转整数IP
5.2 整数IP转字符串IP
5.3 inet_ntoa函数问题 一、TCP网络程序
1.1 服务端初始化
1.1.1 创建套接字
TCP服务器在调用socket函数创建套接字时参数设置如下
协议家族选择AF_INET进行网络通信创建套接字时所需的服务类型应该是SOCK_STREAM因为编写的是TCP服务器SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务协议类型默认设置为0即可
若创建套接字后获得的文件描述符是小于0则套接字创建失败此时就没必要进行后续操作直接终止程序即可
class TcpServer
{
public:void InitSrever();~TcpServer();
private:int _socket_fd;
};void TcpServer::InitSrever()
{//创建套接字_socket_fd socket(AF_INET, SOCK_STREAM, 0);if(_socket_fd 0) {cerr socket fail endl;exit(1);}cout socket success endl;
}TcpServer::~TcpServer() { if(_socket_fd 0) close(_socket_fd); }
TCP服务器创建套接字的做法与UDP服务器基本一致不过创建套接字时TCP使用的是流式服务而UDP使用的是用户数据报服务当析构服务器时可将服务器对应的文件描述符进行关闭
1.1.2 服务端绑定 套接字创建完毕后只是在系统层面上打开了一个文件该文件并没有与网络关联因此创建完套接字后还需要调用bind函数进行绑定操作 绑定的步骤如下
定义struct sockaddr_in结构体变量将服务器网络相关的属性信息填充到该变量中如协议家族、IP地址、端口号等填充服务器网络相关的属性信息时协议家族对应就是AF_INET端口号就是当前TCP服务器程序的端口号。在设置端口号时需要调用htons()函数将端口号由主机序列转为网络序列在设置服务器的IP地址时可以设置为本地环回127.0.0.1表示本地通信。也可以设置为公网IP地址表示网络通信若使用的是云服务器那么在设置服务器的IP地址时不需要绑定固定IP地址直接将IP地址设置为INADDR_ANY即可此时服务器可以从本地任何一张网卡中读取数据。INADDR_ANY本质是0因此在设置时不需要进行网络字节序的转换填充完服务器网络相关的属性信息后调用bind函数进行绑定。绑定实际就是将文件与网络关联若绑定失败没必要进行后续操作直接终止程序即可
TCP服务器初始化时需要服务器的端口号因此在服务器类中需要引入端口号当实例化服务器对象时就需传入一个端口号。而由于当前使用的是云服务器因此在绑定TCP服务器的IP地址时不绑定公网IP地址直接绑定INADDR_ANY即可因此下面代码中没有在服务器类中引入IP地址
class TcpServer
{
public:TcpServer(uint16_t port):_socket_fd(-1),_server_port(port) {}void InitSrever();~TcpServer();
private:int _socket_fd;uint16_t _server_port;
};void TcpServer::InitSrever()
{//创建套接字_socket_fd socket(AF_INET, SOCK_STREAM, 0);if(_socket_fd 0) {cerr socket fail endl;exit(1);}cout socket success endl;//绑定struct sockaddr_in local;memset(local, \0, sizeof local);local.sin_family AF_INET;local.sin_port htons(_server_port);local.sin_addr.s_addr INADDR_ANY;if(bind(_socket_fd, (struct sockaddr*)local, sizeof local) 0) {cerr bind fail endl;exit(2);}cout bind success endl;
}
1.1.3 服务端监听
UDP服务器的初始化操作只有两步创建套接字和绑定。而TCP服务器是面向连接的客户端在正式向TCP服务器发送数据之前需要先与TCP服务器建立连接然后才能与服务器进行通信
因此TCP服务器需要时刻注意是否有客户端发来连接请求此时就需要将TCP服务器创建的套接字设置为监听状态
int listen(int sockfd, int backlog);sockfd需要设置为监听状态的套接字对应的文件描述符backlog全连接队列的最大长度。若有多个客户端同时发来连接请求此时未被服务器处理的连接就会放入连接队列该参数代表的就是这个全连接队列的最大长度一般不要设置太大设置为5或10即可
返回值监听成功返回0监听失败返回-1同时errno被设置 服务器监听代码实现 若监听失败没必要进行后续操作因为监听失败意味着TCP服务器无法接收客户端发来的连接请求直接终止程序即可
void TcpServer::InitSrever()
{//创建套接字_socket_fd socket(AF_INET, SOCK_STREAM, 0);if(_socket_fd 0) {cerr socket fail endl;exit(1);}cout socket success endl;//绑定struct sockaddr_in local;memset(local, \0, sizeof local);local.sin_family AF_INET;local.sin_port htons(_server_port);local.sin_addr.s_addr INADDR_ANY;if(bind(_socket_fd, (struct sockaddr*)local, sizeof local) 0) {cerr bind fail endl;exit(2);}cout bind success endl;//设置服务器监听状态if(listen(_socket_fd, BACKLOG) 0) {cerr listen fail endl;exit(3);}cout listen success endl;
}class TcpServer
{
public:TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port) {}void InitSrever();~TcpServer();
private:int _socket_listen_fd;uint16_t _server_port;
};
初始化TCP服务器时创建的套接字并不是普通的套接字而应该被称为监听套接字。为了表明寓意将代码中套接字的名字由_socket_fd改为_socket_listen_fd初始化TCP服务器时只有创建套接字成功、绑定成功、监听成功此时TCP服务器的初始化才算完成1.2 服务端启动
1.2.1 服务端获取连接
TCP服务器初始化后即可开始运行但TCP服务端在与客户端网络通信前服务端需先获取到客户端的连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);sockfd特定的监听套接字表示从该监听套接字中获取连接addr对端网络相关的属性信息包括协议家族、IP地址、端口号等输出型参数addrlen调用时传入期望读取的addr结构体的长度返回时代表实际读取到的addr结构体的长度这是一个输入输出型参数
返回值获取连接成功则返回接收到的套接字的文件描述符失败返回-1同时错误码被设置 accept函数返回的套接字是什么 调用accept函数获取连接时是从监听套接字中获取的。若accept函数获取连接成功此时会返回接收到的套接字对应的文件描述符
监听套接字与accept函数返回的套接字的作用
监听套接字用于获取客户端发来的连接请求。accept函数会不断从监听套接字中获取新连接accept函数返回的套接字用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接而真正为这些连接提供服务的套接字是accept函数返回的套接字服务端获取连接代码实现 accept函数获取连接时可能会失败但TCP服务器不会因为获取某个连接失败而退出因此服务端获取连接失败后应继续获取连接若要将获取到的连接对应客户端的IP地址和端口号信息进行输出需要调用inet_ntoa函数将整数IP转换成字符串IP(转为主机序列)调用ntohs函数将端口号由网络序列转换成主机序列inet_ntoa函数在底层实际做了两个工作一是将网络序列转换成主机序列二是将主机序列的整数IP转换成字符串风格的点分十进制的IP
void TcpServer::StartUp()
{//获取连接while(true) {struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;}
}
1.2.2 服务端处理请求
TCP服务器已能够获取连接请求接下来要对获取到的连接进行处理。为客户端提供服务的不是监听套接字因为监听套接字获取到一个连接后会继续获取下一个请求连接为对应客户端提供服务的套接字实际是accept函数返回的套接字下面就将其称为服务套接字
为了让通信双方都能看到对应的现象下面实现一个回声TCP服务器服务端在为客户端提供服务时将客户端发来的数据进行输出并且将客户端发来的数据重新发回给客户端即可。当客户端拿到服务端的响应数据后将该数据进行打印输出此时就能确保服务端和客户端能够正常通信了 read函数 ssize_t read(int fd, void *buf, size_t count);fd特定的文件描述符表示从该文件描述符中读取数据buf数据的存储位置表示将读取到的数据存储到该位置count数据的个数表示从该文件描述符中读取数据的字节数
若返回值大于0则表示本次实际读取到的字节数若返回值等于0则表示对端已将连接关闭若返回值小于0则表示读取时出现错误
若客户端将连接关闭了那么此时服务端将套接字中的信息读完后就会读取到0因此若服务端调用read函数后的返回值为0此时服务端就不必再为该客户端提供服务了 write函数 ssize_t write(int fd, const void *buf, size_t count);fd特定的文件描述符表示将数据写入该文件描述符对应的套接字buf需要写入的数据count需要写入数据的字节个数
写入成功返回实际写入的字节数写入失败返回-1同时错误码会被设置 当服务端调用read函数收到客户端的数据后就可以再调用write函数将该数据再响应给客户端 服务端处理请求代码实现 服务端读取数据是服务套接字中读取的写入数据的时候也是写入进服务套接字。服务套接字既可以读取数据也可以写入数据这就是TCP全双工的通信的体现
在从服务套接字中读取客户端发来的数据时若调用read函数后得到的返回值为0或者读取出错了此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标因此文件描述符的资源是有限的若一直占用那么可用的文件描述符就会越来越少因此服务完客户端后要及时关闭对应的文件描述符否则会导致文件描述符泄漏
void TcpServer::StartUp()
{while(true) {//获取连接struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;//处理客户端请求Service(server_socket_fd, client_ip, client_port);}
}void TcpServer::Service(int server_socket_fd, string client_ip, uint16_t client_port)
{char buffer[BUFF_SIZE];while(true) {ssize_t size read(server_socket_fd, buffer, sizeof(buffer) - 1);if(size 0) //读取成功{buffer[size] \0;cout buffer endl;write(server_socket_fd, buffer, size);}else if(size 0) //对端关闭连接{cout client_ip : client_port close endl;break;}else //读取失败{cerr server_socket_fd read error endl;break;}}close(server_socket_fd);cout client_ip : client_port server done endl;
} 1.3 客户端初始化 创建套接字 客户端不需要进行绑定和监听
服务端要进行绑定是因为服务端的IP地址和端口号不能随意改变。而客户端虽然也需要IP地址和端口号但是客户端并不需要程序员手动进行绑定操作客户端连接服务端时系统会自动指定一个端口号给客户端服务端需要进行监听是因为服务端需要通过监听来获取新连接但是不会有人主动连接客户端因此客户端是不需要进行监听操作的
客户端必须要知道要连接的服务端的IP地址和端口号因此客户端除了要有自己的套接字之外还需要知道服务端的IP地址和端口号这样客户端才能够通过套接字向指定服务器进行通信
class TcpClient
{
public:TcpClient(string ip, uint16_t port):_socket_fd(-1),_server_ip(ip),_server_port(port) {}void InitClient();~TcpClient();
private:int _socket_fd;string _server_ip;uint16_t _server_port;
};void TcpClient::InitClient()
{//创建套接字_socket_fd socket(AF_INET, SOCK_STREAM, 0);if(_socket_fd 0) {cerr socket fail endl;exit(1);}
}TcpClient::~TcpClient() { if(_socket_fd 0) close(_socket_fd); } 1.4 客户端启动
1.4.1 发起连接
客户端不需要绑定也不需要监听客户端创建完套接字后可直接向服务端发起连接请求 connect函数 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd特定的套接字表示通过该套接字发起连接请求addr对端网络相关的属性信息包括协议家族、IP地址、端口号等addrlen传入的addr结构体的长度
返回值连接或绑定成功返回0连接失败返回-1同时错误码会被设置 客户端连接服务器代码 客户端不是不需要进行绑定而是不需要程序员手动进行绑定操作当客户端向服务端发起连接请求时系统会给客户端随机指定一个空闲端口号进行绑定。因为通信双方都必须要有IP地址和端口号否则无法唯一标识通信双方。若connect函数调用成功了客户端本地会随机给该客户端绑定一个端口号发送给对端服务器
调用connect函数向服务端发起连接请求时需要传入服务端对应的网络信息否则connect函数也不知道该客户端到底是要向哪一个服务端发起连接请求
void TcpClient::StartUp()
{//发起连接struct sockaddr_in server;memset(server, \0, sizeof server);server.sin_family AF_INET;server.sin_addr.s_addr inet_addr(_server_ip.c_str());server.sin_port htons(_server_port);if(connect(_socket_fd, (struct sockaddr*)server, sizeof(server)) 0) {cout connect success endl;Request(); //发起请求}else {cerr connect fail endl;exit(2);}
}
1.4.2 发起请求
当客户端连接到服务端后客户端就可以向服务端发送数据了可以让客户端将用户输入的数据发送给服务端发送时调用send函数向套接字当中写入数据即可
当客户端将数据发送给服务端后由于服务端读取到数据后还会进行回显因此客户端在发送数据后还需要调用recv函数读取服务端的响应数据然后将该响应数据进行打印以确定双方通信无误
void TcpClient::Request()
{char buffer[BUFF_SIZE];string message;while(true) {cout Pleses Entre#;getline(cin, message);send(_socket_fd, message.c_str(), message.size(), 0);ssize_t size recv(_socket_fd, buffer, sizeof(buffer) - 1, 0);if (size 0){buffer[size] \0;cout server echo# buffer endl;}else if (size 0) {cout server close! endl;break;}else {cerr read error! endl;break;}}
}
通过服务端的IP地址和端口号即可构造出一个客户端对象
void Usage(std::string proc)
{cout Usage: proc server_ip server_port endl;
}
int main(int argc, char* argv[])
{if (argc ! 3) {Usage(argv[0]);exit(1);}string server_ip argv[1];int server_port atoi(argv[2]);TcpClient* client new TcpClient(server_ip, server_port);client-InitClient();client-StartUp();return 0;
} 1.5 网络测试
服务端和客户端均已编写完毕下面进行网络测试。测试时先启动服务端然后使用 netstat 命令进行查看此时能看到 ./server 服务进程该进程当前处于监听状态 然后再通过 ./client IP号 端口号 的形式运行客户端此时客户端就会向服务端发起连接请求服务端获取到请求后就会为该客户端提供服务 当客户端向服务端发送消息后服务端可以通过打印的IP地址和端口号识别出对应的客户端而客户端也可以通过服务端响应回来的消息来判断服务端是否收到了自己发送的消息 若此时客户端退出了那么服务端调用read函数时返回值就为0此时服务端就知道客户端退出了进而会终止对该客户端的服务 此时服务端对该客户端的服务终止了但服务器并没有终止依旧在运行等待下一个客户端的连接请求 1.6 单执行流服务端的弊端
当仅用一个客户端连接服务端时该客户端能够正常享受到服务端的服务 但在这个客户端正在享受服务端的服务时另一个客户端也连接服务器此时虽然在客户端显示连接是成功的服务端并没有显示有新的连接并且这个客户端发送给服务端的消息既没有在服务端进行打印服务端也没有将该数据回显给该客户端 第一个客户端退出后服务端才会将第二个客户端发来的数据进行打印并回显给第二个客户端 单执行流的服务端 通过上图可以看出服务端只有服务完一个客户端后才会服务另一个客户端。因为目前所写的是一个单执行流版的服务器
当服务端调用accept函数获取到连接后就给该客户端提供服务但在服务端提供服务期间可能会有其他客户端发起连接请求但服务完当前客户端后才会accept下一个客户端的连接请求导致服务端一次只能为一个客户端提供服务 客户端为什么会显示连接成功 当服务端在给第一个客户端提供服务期间第二个客户端向服务端发起的连接请求时是成功的只不过服务端没有调用accept函数将该连接获取
实际在底层会维护一个连接队列服务端没有accept的新连接就会放到这个连接队列中而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的因此服务端虽然没有获取第二个客户端发来的连接请求但是在第二个客户端那里显示是连接成功的 解决方案 单执行流的服务器一次只能给一个客户端提供服务此时服务器的资源并没有得到充分利用因此服务端一般是不会编写成单执行流的。要解决这个问题就需要将服务器改为多执行流的此时就要引入多进程或多线程 二、多进程版TCP网络程序
2.1 存在问题
当服务端调用accept函数获取到新连接后不是由当前执行流为该连接对应的客户端提供服务而是当前执行流调用fork函数创建子进程子进程为父进程获取到的连接提供服务
由于父子进程是两个不同的执行流当父进程调用fork创建出子进程后父进程就可以继续从监听套接字中获取新连接而不用关心获取上来的连接对应的客户端是否服务完毕 子进程继承父进程的文件描述符表 文件描述符表是隶属于一个进程的子进程创建后会继承父进程的文件描述符表。如父进程打开了一个文件该文件对应的文件描述符是3子进程的3号文件描述符也会指向这个打开的文件若子进程再创建一个子进程那么孙子进程的3号文件描述符也同样会指向这个打开的文件 当父进程创建子进程后父子进程之间保持独立性此时父进程文件描述符表的变化不会影响子进程。譬如父子进程在使用匿名管道进行通信时父进程先调用pipe函数得到两个文件描述符一个是管道读端的文件描述符一个是管道写端的文件描述符此时父进程创建出来的子进程就会继承这两个文件描述符之后父子进程一个关闭管道的读端另一个关闭管道的写端这时父子进程文件描述符表的变化是不会相互影响的此后父子进程就可以通过这个管道进行单向通信了
对于套接字文件也是一样的父进程创建的子进程也会继承父进程的套接字文件此时子进程就能够对特定的套接字文件进行读写操作进而完成对对应客户端的服务 等待子进程问题 当父进程创建出子进程后父进程是需要等待子进程退出的否则子进程会变成僵尸进程进而造成内存泄漏。因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待
阻塞式等待与非阻塞式等待
若服务端采用阻塞的方式等待子进程那么服务端还是需要等待服务完当前客户端才能继续获取下一个连接请求此时服务端仍然是以一种串行的方式为客户端提供服务若服务端采用非阻塞的方式等待子进程虽然在子进程为客户端提供服务期间服务端可以继续获取新连接但此时服务端就需要将所有子进程的PID保存下来并且需要不断花费时间检测子进程是否退出且编码较为复杂
服务端要等待子进程退出无论采用阻塞式等待还是非阻塞式等待都不尽人意。此时可以考虑让服务端不等待子进程退出的方案 2.2 捕捉SIGCHLD信号 当子进程退出时会给父进程发送SIGCHLD信号若父进程将SIGCHLD信号进行捕捉并将该信号的处理动作设置为忽略此时父进程可以继续从监听套接字中获取新连接 该方案实现较为简单较为推荐 void TcpServer::StartUp()
{//设置忽略SIGCHLD信号signal(SIGCHLD, SIG_IGN);while(true) {//获取连接struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;//处理客户端请求pid_t id fork();if(id 0) { //childService(server_socket_fd, client_ip,client_port);exit(4);}}
} 网络测试 重新编译程序运行服务端后可以通过以下监控脚本对服务进程进行监控 while :; do ps axj | head -1 ps axj | grep tcp_server | grep -v grep;echo ######################;sleep 1;done一开始没有客户端连接该服务器此时服务进程只有一个该服务进程就是不断获取新连接的进程而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的 此时启动一个客户端让该客户端连接服务器此时服务进程就会调用fork函数创建出一个子进程由该子进程为这个客户端提供服务 若再有一个客户端连接服务器此时服务进程会再创建出一个子进程为这个客户端提供服务 这两个客户端分别由两个不同的执行流提供服务因此这两个客户端可以同时享受到服务发送给服务端的数据都能够在服务端输出并且服务端对两个客户端的数据都会进行响应 当客户端陆续退出后在服务端对应为之提供服务的子进程也会退出但无论如何服务端都至少会有一个服务进程这个服务进程的任务就是不断获取新连接 2.3 孙子进程提供服务
爷爷进程在服务端调用accept函数获取客户端连接请求的进程爸爸进程爷爷进程调用fork函数创建出来的进程孙子进程爸爸进程调用fork函数创建出来的进程该进程调用Service函数为客户端提供服务
爸爸进程创建完孙子进程后立刻退出此时服务进程爷爷进程调用wait/waitpid函数等待爸爸进程就能立刻等待成功此时孙子进程变成孤儿进程被1号进程领养此后服务进程就能继续调用accept函数获取其他客户端的连接请求。不需要处理孙子进程其资源由系统释放 关闭对应的文件描述符 服务进程爷爷进程调用accept函数获取到新连接后会让孙子进程为该连接对应的服务端提供服务此时服务进程已经将文件描述符表继承给了爸爸进程而爸爸进程又会调用fork函数创建出孙子进程然后再将文件描述符表继承给孙子进程
而父子进程创建后其各自的文件描述符表是独立的不会相互影响。因此服务进程在调用fork函数后服务进程就不需要再关心刚才从accept函数获取到的文件描述符了此时服务进程就可以调用close函数将该文件描述符进行关闭
对于爸爸进程和孙子进程来说是不需要关心从服务进程爷爷进程继承下来的监听套接字的因此服务进程可以将监听套接字关掉
对于服务进程来说调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符服务套接字若服务进程不及时关掉不用的文件描述符最终服务进程中可用的文件描述符就会越来越少对于孙子进程而言还是建议关闭从服务进程继承下来的监听套接字。实际就算不关闭监听套接字最终也只会导致这一个文件描述符泄漏但还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某些误操作此时就会对监听套接字当中的数据造成影响实际编码时在爸爸进程fork之前将其监听套接字关闭孙子进程继承的文件描述符表中自然没有监听套接字了
void TcpServer::StartUp()
{while(true) {//获取连接struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;//处理客户端请求pid_t id fork();if(id 0) { //爸爸进程close(_socket_listen_fd);//关闭监听套接字if(fork() 0) exit(4);//服务进程子进程直接退出//孙子进程处理Service(server_socket_fd, client_ip,client_port);exit(5);}close(server_socket_fd);//服务进程关闭连接客户端时获取的文件描述符waitpid(id, nullptr, WNOHANG);//等待爸爸进程立即成功}
} 网络测试 while :; do ps axj | head -1 ps axj | grep tcp_server | grep -v grep;echo ######################;sleep 1;done此时没有客户端连接服务器只监控到一个服务进程该服务进程正在等待客户端的连接请求 此时启动一个客户端让该客户端连接服务端此时服务进程会创建出爸爸进程爸爸进程再创建出孙子进程之后爸爸进程就会立刻退出而由孙子进程为客户端提供服务。所以只看到了两个服务进程其中一个是一开始用于获取连接的服务进程还有一个就是孙子进程该进程为当前客户端提供服务其PPID为1表明这是一个孤儿进程 启动第二个客户端连接服务器后就又会创建出一个孤儿进程为该客户端提供服务 两个客户端是由两个不同的孤儿进程提供服务的因此是能够同时享受服务的可以看到这两个客户端发送给服务端的数据都能够在服务端输出并且服务端也会对这两个客户端的数据进行响应 当客户端全部退出后对应为客户端提供服务的孤儿进程也会跟着退出这时这些孤儿进程会被系统回收而最终剩下那个获取连接的服务进程 三、多线程版TCP网络程序
创建进程的成本是很高的创建进程时需要创建该进程对应的进程控制块task_struct、进程地址空间mm_struct、页表等结构。而创建线程的成本比创建进程的成本会小得多因为线程本质是在进程地址空间内运行创建出来的线程会共享该进程的大部分资源因此在实现多执行流的服务器时最好采用多线程进行实现
服务进程调用accept函数获取到一个新连接后可创建一个线程让该线程为对应客户端提供服务
主线程创建出新线程后也是需要等待新线程退出回收资源的否则也会造成资源浪费的问题。但对于线程来说若不想让主线程等待新线程退出可以让创建出来的新线程调用pthread_detach函数进行线程分离当这个线程退出时系统会自动回收该线程所对应的资源。此时主线程就可以继续调用accept函数获取新连接而让新线程去服务对应的客户端 各个线程共享同一张文件描述符表 文件描述符表维护的是进程与文件之间的对应关系因此一个进程对应一张文件描述符表。而主线程创建出来的新线程依旧属于这个进程因此创建线程时并不会为该线程创建独立的文件描述符表所有的线程看到的都是同一张文件描述符表 当主线程调用accept函数获取到一个文件描述符后新线程是能够直接访问这个文件描述符的
虽然新线程能够直接访问主线程accept上来的文件描述符但此时新线程并不知道其所服务的客户端对应的是哪一个文件描述符因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符即告诉每个新线程在服务客户端时应该对哪一个套接字进行操作 参数结构体 实际新线程在为客户端提供服务时调用Service函数而调用Service函数时是需要传入三个参数的分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数而实际在调用pthread_create函数创建新线程时只能传入一个类型为void*的参数
这时可以设计一个参数结构体ThreadDate这三个参数可以存放到ThreadDate结构体中当主线程创建新线程时就可以定义一个ThreadDate对象将客户端对应的套接字、IP地址和端口号设置进这个ThreadDate对象中然后将Param对象的地址作为新线程执行例程的参数进行传入
此时新线程在执行例程当中再将这个void*类型的参数强转为Param*类型然后就能够拿到客户端对应的套接字IP地址和端口号进而调用Service函数为对应客户端提供服务
class ThreadDate
{
public:ThreadDate(int fd, string ip,uint16_t port):_server_socket_fd(fd),_client_ip(ip),_client_port(port) {}~ThreadDate() {}
public:int _server_socket_fd;//accept获取连接得到文件描述符用于服务string _client_ip;uint16_t _client_port;
}; 文件描述符关闭的问题 所有线程看到的都是同一张文件描述符表因此当某个线程要对文件描述符表做某种操作时不仅要考虑当前线程还要考虑其他线程。
对于主线程accept来的文件描述符主线程不能对其进行关闭操作该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭对于监听套接字虽然创建出来的新线程不必关心监听套接字但新线程不能将监听套接字对应的文件描述符关闭否则主线程就无法从监听套接字当中获取新连接了Service函数定义为静态成员函数 由于调用pthread_create函数创建线程时新线程的执行例程是一个参数为void*返回值为void*的函数。若要将这个执行例程定义到类内就需要将其定义为静态成员函数否则这个执行例程的第一个参数是隐藏的this指针
在线程的执行例程中会调用Service函数由于执行例程是静态成员函数静态成员函数无法调用非静态成员函数因此需要将Service函数定义为静态成员函数恰好Service函数内部进行的操作都不涉及类内数据的修改因此直接在Service函数前面加上一个static即可
class TcpServer
{
public:TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port) {}void InitServer();void StartUp();static void Service(int, string, uint16_t);~TcpServer();
private:int _socket_listen_fd;uint16_t _server_port;
};
void TcpServer::StartUp()
{while(true) {//获取连接struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;//处理客户端请求ThreadDate* ptr new ThreadDate(server_socket_fd, client_ip, client_port);pthread_t thread_id;pthread_create(thread_id, nullptr, HandlerClient, (void*)ptr);/*应将ThreadDate数据开辟在堆区若开辟在主线程栈区主线程循环accept并处理客户端请求时会修改TheadDate内数据*/}
}void* TcpServer::HandlerClient(void* args)
{pthread_detach(pthread_self());//线程分离资源由系统回收ThreadDate* ptr (ThreadDate*)args;Service(ptr-_server_socket_fd, ptr-_client_ip, ptr-_client_port);delete ptr;return nullptr;
} 网络测试 监控时使用的不再是 ps -axj 命令而是 ps -aL 命令
while :; do ps -aL|head -1ps -aL|grep tcp_server;echo ####################;sleep 1;done启动服务端通过监控发现此时只有一个服务线程主线程现在在等待客户端的连接请求 当一个客户端连接到服务端后此时主线程就会为该客户端构建一个参数结构体然后创建一个新线程将该参数结构体的地址作为参数传递给这个新线程此时该新线程就能够从这个参数结构体中提取出对应的参数然后调用Service函数为该客户端提供服务因此在监控中显示了两个线程 当第二个客户端发来连接请求时主线程会进行相同的操作最终再创建出一个新线程为该客户端提供服务此时就有了三个线程 由于为这两个客户端提供服务的是两个不同的执行流因此这两个客户端可同时享受服务端提供的服务发送给服务端的消息都能够在服务端打印并且这两个客户端都能够收到服务端的回显数据 此时无论有多少个客户端发来连接请求在服务端都会创建出相应数量的新线程为对应客户端提供服务而当客户端一个个退出后为其提供服务的新线程也就会相继退出最终就只剩下主线程在等待新连接的到来 四、线程池版TCP网络程序 单纯多线程存在的问题 每当有新连接到来时服务端的主线程都会重新为该客户端创建为其提供服务的新线程而当服务结束后又会将该新线程销毁。这样不仅麻烦且效率低下每当连接到来的时候服务端才创建对应提供服务的线程若有大量的客户端连接请求此时服务端要为每一个客户端创建对应的服务线程。计算机中的线程越多CPU的压力就越大因为CPU要不断在这些线程之间来回切换此时CPU在调度线程的时候线程和线程之间切换的成本就会变得很高。此外一旦线程太多每一个线程再次被调度的周期就变长了而线程是为客户端提供服务的线程被调度的周期变长客户端也体验也会变差解决方案 可以在服务端预先创建一批线程当有客户端请求连接时就让这些线程为客户端提供服务此时客户端一来就有线程为其提供服务而不是当客户端来了才创建对应的服务线程当某个线程为客户端提供完服务后不要让该线程退出而是让该线程继续为下一个客户端提供服务若当前没有客户端连接请求则可以让该线程先进入休眠状态当有客户端连接到来时再将该线程唤醒服务端创建的这一批线程的数量不能太多CPU的压力也就不会太大。此外若有客户端连接到来但此时这一批线程都在给其他客户端提供服务这时服务端不应该再创建线程而应该让这个新来的连接请求在全连接队列进行排队等服务端这一批线程中有空闲线程后再将该连接请求获取上来并为其提供服务引入线程池 要解决问题就需在服务端引入线程池线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价还能够保证内核充分利用防止过分调度调度周期过长
在线程池中存在一个任务队列当有新的任务到来的时候就可以将任务Push到线程池中在线程池中默认创建10个线程这些线程不断检测任务队列中是否有任务若有任务就取出任务然后调用该任务对应的Run函数对该任务进行处理若线程池中没有任务当前线程就会进入休眠状态
下面直接将线程池的代码接入到当前的TCP服务器下面只会讲解线程池接入的方法若对线程池的实现有疑问的可以去阅读博主的博客《理解与实现线程池》
理解与实现线程池https://blog.csdn.net/GG_Bruse/article/details/129616793 服务类新增线程池成员 服务端引入线程池因此在服务类中需要新增一个指向线程池的指针成员
在构造线程池对象时可以指定线程池中线程的个数此时默认线程的个数为10构造线程池时线程池中的若干线程就会创建出来而这些线程创建出来后就会不断检测任务队列从任务队列中取出任务进行处理当服务进程调用accept函数获取到一个连接请求后就会根据该客户端的套接字、IP地址以及端口号构建出一个任务然后调用线程池提供的Push接口将该任务塞入任务队列
实际上就是一个生产者消费者模型其中服务进程就作为了任务的生产者而后端线程池中的若干线程就不断从任务队列当中获取任务进行处理承担的就是消费者的角色其中生产者和消费者的交易场所就是线程池中的任务队列
class TcpServer
{
public:TcpServer(uint16_t port):_socket_listen_fd(-1),_server_port(port),_thread_pool(ThreadPoolTask::GetThreadPool()) {}void InitServer();void StartUp();static void* HandlerClient(void*);static void Service(int, string, uint16_t);~TcpServer();
private:int _socket_listen_fd;uint16_t _server_port;unique_ptrThreadPoolTask _thread_pool;
};
void TcpServer::StartUp()
{_thread_pool-Run();//启动线程池while(true) {//获取连接struct sockaddr_in foreign;memset(foreign, \0, sizeof foreign);socklen_t length sizeof foreign;int server_socket_fd accept(_socket_listen_fd, (struct sockaddr*)foreign, length);if(server_socket_fd 0) {cerr accept fail endl;continue;}string client_ip inet_ntoa(foreign.sin_addr);uint16_t client_port ntohs(foreign.sin_port);cout New Link: [ server_socket_fd ] [ client_ip ] [ client_port ] endl;//构造任务并推送到任务队列中Task task(server_socket_fd, client_ip, client_port, Service);_thread_pool-PushTask(task);}
} 设计任务类 该任务类中需要包含accept客户端对应的套接字、IP地址、端口号表示该任务是为哪一个客户端提供服务对应操作的套接字是哪一个
任务类中需包含一个仿函数方法当线程池中的线程取到任务后就会直接调用仿函数对该任务进行处理而实际处理这个任务的方法是服务类中的Service函数服务端就是通过调用Service函数为客户端提供服务的
typedef void(*fun_t)(int, std::string, uint16_t);
class Task
{
public:Task() {}Task(int sock, std::string client_ip, int client_port, fun_t handler) : _server_socket_fd(sock), _client_ip(client_ip), _client_port(client_port), _handler(handler) {}//任务处理函数void operator()(const std::string name) {_handler(_server_socket_fd, _client_ip, _client_port);}
private:int _server_socket_fd;std::string _client_ip;uint16_t _client_port;fun_t _handler;
};
实际可以让服务器处理不同的任务当前服务器只是在进行字符串的回显处理而实际要怎么处理这个任务完全是由任务类中的_handler成员来决定的
若想要让服务器处理其他任务只需要修改()的重载函数就行了而服务器的初始化、启动服务器以及线程池的代码都是不需要更改的这被称为把通信功能和业务逻辑在软件上做解耦 网络测试 while :; do ps -aL|head -1ps -aL|grep tcp_server;echo ####################;sleep 1;done运行服务端后就算没有客户端发来连接请求此时在服务端就已经有了11个线程其中有一个是接收新连接的服务线程而其余的5个是线程池中为客户端提供服务的线程 当客户端连接服务器后服务端的主线程就会获取该客户端的连接请求并将其封装为一个任务对象后放入任务队列此时线程池中的10个线程就会有一个线程从任务队列当中获取到该任务并执行该任务的处理函数为客户端提供服务 当第二个客户端发起连接请求时服务端也会将其封装为一个任务类放入任务队列然后线程池中的线程再从任务队列中获取到该任务进行处理此时也是不同的执行流为这两个客户端提供的服务因此这两个客户端是能够同时享受服务的 无论有多少客户端发来请求在服务端都只会有线程池中的10个线程为之提供服务线程池中的线程个数不会随着客户端连接的增多而增多这些线程也不会因为客户端的退出而退出
线程池版TCP程序https://github.com/GG-Bruse/BaoLinux/tree/master/code/socket/TCP/TCP_ThreadPool 五、地址转换函数
5.1 字符串IP转整数IP inet_aton函数 int inet_aton(const char *cp, struct in_addr *inp);cp待转换的字符串IP。inp转换后的整数IP输出型参数
返回值若转换成功则返回一个非零值若输入的地址不正确则返回零值 inet_addr函数 in_addr_t inet_addr(const char *cp);参数cp待转换的字符串IP
返回值若输入地址有效则返回转换后的整数IP若无效则返回INADDR_NONE-1 inet_pton函数 int inet_pton(int af, const char *src, void *dst);af参数协议家族src参数待转换的字符串IPdst参数转换后的整数IP输出型参数
返回值说明
若转换成功则返回1若输入的字符串IP无效则返回0若输入的协议家族af无效则返回-1并将errno设置为EAFNOSUPPORT5.2 整数IP转字符串IP inet_ntoa函数 char *inet_ntoa(struct in_addr in);参数in待转换的整数IP
返回值返回转换后的字符串IP inet_ntop函数 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);af参数协议家族src参数待转换的整数IPdst参数转换后的字符串IP输出型参数size参数用于指明dst中可用的字节数
返回值若转换成功则返回一个指向dst的非空指针若转换失败则返回NULL。 注意 最常用的两个转换函数是inet_addr和inet_ntoa因为这两个函数足够简单。这两个函数的参数就是需要转换的字符串IP或整数IP而这两个函数的返回值就是对应的整数IP和字符串IP其中inet_pton和inet_ntop函数不仅可以转换IPv4的in_addr还可以转换IPv6的in6_addr因此这两个函数中对应的参数类型是void*转换函数都是为了满足某些打印场景的或者做某些数据分析如网络安全方面的数据分析5.3 inet_ntoa函数问题 inet_ntoa函数可以将4字节的整数IP转换成字符串IP其中该函数返回的这个转换后的字符串IP是存储在静态存储区的不需要调用者手动进行释放。若多次调用inet_ntoa函数此时就会出现数据覆盖的问题 inet_ntoa函数内部只在静态存储区申请了一块区域导致inet_ntoa函数第二次转换的结果就会覆盖第一次转换的结果 若要多次调用inet_ntoa函数那么就要及时保存inet_ntoa的转换结果 并发场景下的inet_ntoa函数 inet_ntoa函数内部只在静态存储区申请了一块区域用于存储转换后的字符串IP那么在线程场景下这块区域就叫做临界区多线程在不加锁的情况下同时访问临界区必然会出现异常情况。并且在APUE中也明确提出inet_ntoa不是线程安全的函数
#include iostream
#include netinet/in.h
#include arpa/inet.h
#include pthread.h
#include unistd.hvoid* Func1(void* arg)
{struct sockaddr_in* p (struct sockaddr_in*)arg;while (1){char* ptr1 inet_ntoa(p-sin_addr);std::cout ptr1: ptr1 std::endl;sleep(1);}
}
void* Func2(void* arg)
{struct sockaddr_in* p (struct sockaddr_in*)arg;while (1){char* ptr2 inet_ntoa(p-sin_addr);std::cout ptr2: ptr2 std::endl;sleep(1);}
}
int main()
{struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr 0;addr2.sin_addr.s_addr 0xffffffff;pthread_t tid1 0;pthread_create(tid1, nullptr, Func1, addr1);sleep(1);pthread_t tid2 0;pthread_create(tid2, nullptr, Func2, addr2);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}但是实际在centos7上测试时在多线程场景下调用inet_ntoa函数并没有出现问题可能是该函数内部的实现加了互斥锁这就跟接口本身的设计也是有关系的 在多线程环境下更加推荐使用inet_ntop函数进行转换因为该函数是由调用者自己提供缓冲区保存转换结果的可以规避线程安全的问题