赞
踩
喜欢的点赞,收藏,关注一下把!
TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们写代码的方式看一下。
#pragma once #include <iostream> #include <string> using namespace std; enum { USAGG_ERR = 1, }; class tcpServer { public: tcpServer(const uint16_t port) : _port(port), _listensock(-1) { } void initServer() { } void start() { } ~tcpServer() { } private: uint16_t _port; int _sock; };
我们已经知道,云服务器不允许绑定公网IP,所以这里我们直接使用INADDR_ANY绑定任意IP,端口号自己指定就行了。
#include"tcpServer.hpp" #include<memory> void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " local_port\n\n"; } // ./tcpserver port int main(int argc,char* argv[]) { if(argc != 2) { Usage(argv[0]); exit(USAGG_ERR); } uint16_t serverport=atoi(argv[1]); unique_ptr<tcpServer> tsv(new tcpServer(serverport)); tsv->initServer(); tsv->start(); return 0; }
初始化服务器
进行网络通信首先要创建套接字。
不过今天这里,socket第二个参数我们要写成 SOCK_STREAM 对应TCP协议面向字节流。
void initServer()
{
// 1.创建socket套接字
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
}
}
_sock<0,说明套接字失败,那就没有必须进行了,但是我们这里封装一个日志函数。日志是有日志等级的。
操作系统也是有日志的。
cat /var/log/messages //查看日志
日志等级有些是warning,error,fatal等
未来我们也想这些消息是以不同等级显示出来的,必须还要以特定的格式显示出来。
#pragma once #include<iostream> #include<string> #define DUGNUM 0 #define NORMAL 1 #define WARNING 2 #define ERROR 3 #define FATAL 4 void logMessage(int level,const std::string& message) { //[日志等级] [时间戳/时间] [pid] [message] //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败] std::cout<<message<<std::endl; //暂定 };
未来在输出消息的时候,消息都是规范化的。统一调用这个函数,可以往显示器上面打,也可以往文件中写。
这个日志函数不完整,我们先把TCP服务端客户端写完再来完善。
enum { USAGG_ERR = 1, SOCKET_ERR, BIND_ERR, }; void initServer() { // 1.创建socket文件套接字对象 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { logMessage(FATAL, "socket create error"); exit(SOCKET_ERR); } logMessage(NORMAL, "socker create success"); // 2.bind 绑定自己的网络消息 port和ip struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 任意地址bind,服务器真实写法 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { logMessage(FATAL, "bind socket error"); exit(BIND_ERR); } logMessage(NORMAL, "bind socket success"); }
前面UDP服务端初始这两步做完到这里就完了,但是TCP服务器是面向连接的,所以当别人给我发数据时候不能直接发数据,必须先和我建立连接,这就意味着服务器必须时时刻刻知道他向我发起连接请求。所以必须有第3步 设置socket 为监听状态(为了获取新连接)
backlog:底层全连接队列的长度,这个参数在后面TCP协议的时候说 。
static const int backlog = 5; enum { USAGG_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; void initServer() { // 1.创建socket文件套接字对象 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { logMessage(FATAL, "socket create error"); exit(SOCKET_ERR); } logMessage(NORMAL, "socker create success"); // 2.bind 绑定自己的网络消息 port和ip struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 任意地址bind,服务器真实写法 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { logMessage(FATAL, "bind socket error"); exit(BIND_ERR); } logMessage(NORMAL, "bind socket success"); // 3.设置socket为监听状态 if (listen(_listensock, backlog) < 0) // backlog 底层链接队列的长度 { logMessage(FATAL, "listen socket error"); exit(LISTEN_ERR); } logMessage(NORMAL, "listen socker success"); }
初始化服务器:1.创建socket ,2.bind ,3.设置socket 为监听状态
启动服务器
TCP不能直接发数据 ,因为它是面向连接的。通信之前必须要先获取连接,因此首先要获取新连接。
从这个sockfd这里获取新连接。
accept函数后两个参数和recvfrom是一模一样的,这两个参数的含义也是一样的都是输入输出型函数,将来谁连的我,远端的客户端的ip和port是多少。所以需要这两个参数把客户端消息获取上来。
这些都不重要,最重要的是accept的返回值
成功时这个函数会从已接受的socket返回一个文件描述符!失败返回-1错误码被设置。
这里问题就来了,调用accept它的返回值也是文件描述符,而我们自己也建立一个文件描述符,那这两个文件描述符是什么意思?
下面举个例子理解:
今天我和一群朋友去杭州西湖旅游,玩累了准备找个地方吃饭,假设来了一个地方都是卖鱼的,王家鱼庄、李家鱼庄、张家鱼庄等等。每一家鱼庄门口都有一个拉客的人,张三是王家鱼庄的门口拉客的人。我们走着走着张三过来了,小哥小哥你们要不要吃饭啊,我们这里的鱼都是从西湖打上来的。我们感觉可以试试,于是张三就带我和我的朋友到王家鱼庄,到了门口张三就向大厅呼唤李四过来接客把我们带进去,李四过来招呼我们,给我们倒水介绍特殊菜。当我们在享受李四给我们带来的服务时,张三去那了?张三自己有自己的业务,他把我们招呼过来之后,转头就走了,又跑到路边找下一位客人了。当我和我的朋友在吃饭的期间,发现我们周边越来越热闹了,张三带着客人来然后在门口喊着让其他人招呼客人。李四给我们提供服务,王五给别的客人提供服务等等。张三一直干着这一件事情。
张三 : 拉客
李四、王五、赵六。。。:提供服务
张三就相当于我们传给accept的创建好的文件描述符,
李四、王五、赵六。。。就相当于accept返回文件描述符
一个服务器可能被多个客户端来连接,李四、王五、赵六。。。每一个都是对应一个文件描述符对外提供服务的, 未来我们一旦建立好连接,服务器不能用创建好的文件描述符和客户端通信,就好比不能用张三给客人提供服务,而应该让accept的返回值文件描述符来给用户提供服务。
class tcpServer { public: //。。。 void start() { for (;;) { // 4.获取新链接 //这个结构体用来获取谁连接的我 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); //这个sock 用来和client进行通信的文件描述符 int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue;//获取连接失败,但不影响获取下一个连接 } logMessage(NORMAL, "accpet a new link success); //cout << "sock: " << sock << endl; } //。。。 private: uint16_t _port; //int _sock; int _listensock;//不是用来进行数据通信的,它是用来监听连接的到来,获取新连接的 };
接下来就用这个sock和客户端进行通信了
void start() { for (;;) { // 4.获取新链接 //这个结构体用来获取谁连接的我 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); //这个sock 用来和client进行通信的文件描述符 int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue;//获取连接失败,但不影响获取下一个连接 } logMessage(NORMAL, "accpet a new link success); //cout << "sock: " << sock << endl;//可以看到新的文件描述符 // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! // version 1 这里我们后面会写好几个版本。因此先写第一个简单版本 serverIO(sock);//用这个函数对外提供服务 close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏 } void serverIO(int &sock) { char buffer[1024]; while (true) { // 读 ssize_t n = read(sock, buffer, sizeof(buffer)); if (n > 0)//返回读到多少字节 { //目前我们把读到的数据当成字符串,截至目前 buffer[n] = 0; cout << "recv message: " << buffer << endl; // 写 string outbuffer = buffer; outbuffer += "server [respond]"; write(sock, outbuffer.c_str(), outbuffer.size()); } else if (n == 0)//读到文件末尾 { // 代表clien退出 logMessage(NORMAL, "client quit, me to!"); break; } } }
服务端完整代码
#pragma once #include "logMessage.hpp" #include <iostream> #include <string> #include <stdlib.h> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; enum { USAGG_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; const int backlog = 5; class tcpServer { public: tcpServer(const uint16_t port) : _port(port), _listensock(-1) { } void initServer() { // 1.创建socket文件套接字对象 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { logMessage(FATAL, "socket create error"); exit(SOCKET_ERR); } logMessage(NORMAL, "socker create success"); // 2.bind 绑定自己的网络消息 port和ip struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; // 任意地址bind,服务器真实写法 if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { logMessage(FATAL, "bind socket error"); exit(BIND_ERR); } logMessage(NORMAL, "bind socket success"); // 3.设置socket为监听状态 if (listen(_listensock, backlog) < 0) // backlog 底层链接队列的长度 { logMessage(FATAL, "listen socket error"); exit(LISTEN_ERR); } logMessage(NORMAL, "listen socker success"); } void start() { for (;;) { // 4.获取新链接 //这个结构体用来获取谁连接的我 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); //这个sock 用来和client进行通信的文件描述符 int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue;//获取连接失败,但不影响获取下一个连接 } logMessage(NORMAL, "accpet a new link success); //cout << "sock: " << sock << endl;//可以看到新的文件描述符 // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! // version 1 这里我们后面会写好几个版本。因此先写第一个简单版本 serverIO(sock);//用这个函数对外提供服务 close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏 } void serverIO(int &sock) { char buffer[1024]; while (true) { // 读 ssize_t n = read(sock, buffer, sizeof(buffer)); if (n > 0)//返回读到多少字节 { //目前我们把读到的数据当成字符串,截至目前 buffer[n] = 0; cout << "recv message: " << buffer << endl; // 写 string outbuffer = buffer; outbuffer += "server [respond]"; write(sock, outbuffer.c_str(), outbuffer.size()); } else if (n == 0)//读到文件末尾 { // 代表clien退出 logMessage(NORMAL, "client quit, me to!"); break; } } } ~tcpServer() { } private: // string _ip; uint16_t _port; int _listensock; };
netstat -nltp //查看处于监听的TCP
#pragma once #include <iostream> #include <string> #include <stdlib.h> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; class tcpClient { public: tcpClient(const string &ip, const uint16_t &port) : _serverip(ip), _serverport(port), _sockfd(-1) { } void initClient() { } void run() { } ~tcpClient() { } private: string _serverip; uint16_t _serverport; int _sockfd; };
#include"tcpClient.hpp" #include<memory> void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " local_ip local_port\n\n"; } // ./tcpClient serverip serverport int main(int argc,char* argv[]) { if(argc != 3) { Usage(argv[0]); exit(1); } string serverip=argv[1]; uint16_t serverport=atoi(argv[2]); unique_ptr<tcpClient> utc(new tcpClient(serverip,serverport)); utc->initClient(); utc->run(); return 0; }
初始化客服端
void initClient()
{
// 1.创建socket套接字
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
cerr << "socket fail" << endl;
exit(2);
}
// 2.tcp的客户端要不要bind,要不要显示bind? 要bind,不需要显示bind
// 要不是listen 监听?
// 要不要accept?
}
因为客户端和服务端通信需要【源ip ,目的ip】,【源端口,目的端口】,所以要bind。但是不需要显示bind,因为如果bind特定的端口,如果两个客户端都bind一样的端口,谁先启动谁成功bind,另一个就不能启动了。
下一个问题,我们的客户端要不要listen?
不需要,服务器 listen是因为有人要连接它,客户端是发起连接的。
那客户端要不要accept?
不需要,服务器accept也是因为有人要连接它,客户端是是发起连接的。
那客户端到底要什么呢?
要发起连接!
发现连接我们写到启动客户端里
启动客户端
第一个参数通过那个套接字发起连接
第二个参数你要向那个ip和port的服务端发起连接
第三个参数是这个结构体的长度
以前在udp是第一次sendto发现没有bind会调用bind绑定ip和port,而tcp这里是在connect会帮bind。
void run() { // 2.发起链接 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(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0) { cerr << "socker connect fail" << endl; } else { string msg; while (true) { // 发 cout << "Please Enter# "; getline(cin, msg); write(_sockfd, msg.c_str(), msg.size()); // 收 char buffer[1024]; ssize_t n = read(_sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; cout <<"server 回显: " <<buffer << endl; } else { break; } } } }
客户端完整代码
#pragma once #include <iostream> #include <string> #include <stdlib.h> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> using namespace std; class tcpClient { public: tcpClient(const string &ip, const uint16_t &port) : _serverip(ip), _serverport(port), _sockfd(-1) { } void initClient() { // 1.创建socket套接字 _sockfd = socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) { cerr << "socket fail" << endl; exit(2); } // 2.要不要bind,要不要显示bind? 要bind,不需要显示bind // 要不是listen 监听? 不需要 // 要不要accept? 不需要 // 自己是发送链接的一方 } void run() { // 2.发起链接 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(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0) { cerr << "socker connect fail" << endl; } else { string msg; while (true) { // 发 cout << "Please Enter# "; getline(cin, msg); write(_sockfd, msg.c_str(), msg.size()); // 收 char buffer[1024]; ssize_t n = read(_sockfd, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; cout <<"server 回显: " <<buffer << endl; } else { break; } } } } ~tcpClient() { //这里也可以不主动关,我们知道文件描述符的生命周期随进程,客户端进程退了会自动帮关的 if(_sockfd >= 0) close(_sockfd); } private: string _serverip; uint16_t _serverport; int _sockfd; };
为什么服务这里打印出来的文件描述符是4呢?
因为默认打开三个文件,0,1,2被占了,3被listensock占了,所以这里打印的是4
netstat -ntap //查看所有处于tcp的进程
ESTABLISHED :建立连接
我们确实看到客户端发起的连接已经被服务端看到了并且连接了。
这里的问题为什么有两条连接呢?正常情况下不是一条连接吗?
一般而言,TCP确实在查找的时候建立连接成功,只会有一条连接!!!
但是今天我们做测试,客户端和服务端是在一台机器上的!!!
如果是两台主机,你是服务端你看到的就是上面的,你是客户端你看到的是下面的。即便只有一条连接也是全双工的!
这里可以看到客户端关了服务端立马读到了,客户端在连这个文件又变成4了,这说明客户端一关闭服务端就将刚刚的文件描述符关了,关了之后你在连接我给你的还是4,此时文件描述符就被重复使用了。
注意看,当我又开一个客户端去连接然后给服务端发送消息的时候,服务端并不会显示,只有当我把上一个客户端关闭后,然后才获取到新连接,这个文件描述符还是4,才会把我发的消息接收。
这是因为刚才所写的服务器,我们获取一个新连接之后,然后进程就去serverIO提供死循环服务了。人家不退,服务器就一直在serverIO给人家提供服务。
那怎么能保证并发的,给多个人提供服务呢?
下面我们就把刚才写的版本改一下:
多进程两个版本,多线程版本,线程池版本。
获取新连接之后创建子进程,创建子进程,父进程的文件描述符会被子进程继承的,文件描述符所指的文件也都是一样的。所以说父进程曾经打开的listensock以及sock子进程都能看到。
创建子进程,让子进程对外提供服务。
这里要注意父进程的文件描述符被子进程继承下来了,但是父进程可是打开了多个文件描述符,所以子进程最少把自己的不需要的文件描述符关掉。
void start() { for (;;) { // 4.获取新链接 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue; } // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock); logMessage(NORMAL, "accpet a new link success,get new sock"); cout << "sock: " << sock << endl; // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! // version2 多进程 int fd=fork(); if(fd == 0) //child { //关闭不需要的监听文件描述符 close(_listensock); serverIO(sock); close(sock); exit(0); } //父进程 }
那父进程要干什么呢?
根据以前在进程哪里所学知识,父进程当然是要阻塞或者非阻塞等待回收子进程的资源了,否则子进程退出变成僵尸进程了,就造成内存资源泄漏了
但是这里要等待的时候,选择阻塞式等待还是非阻塞等待?
选择阻塞式等待,那不还是串行执行吗,属于脱裤子放屁多此一举创建子进程。选择非阻塞式等待,万一没有新连接来了一直在accept哪里等着连接,对子进程资源可能并没有回收干净造成内存资源泄漏。所以选择非阻塞式等待并不好!
如果非要让你阻塞式等待,要怎么做?
这里是这样做的,让子进程关闭listensock之后,子进程在创建一个子进程也就是孙子进程,让子进程退出!孙子进程提供服务。因为子进程退出了所以父进程等待会立马成功,然后继续向下执行代码。虽然父进程回收了子进程资源,但是并不影响孙子进程提供服务,等孙子进程提供完服务自己退出。你是孙子进程和父进程没有半毛钱关系(各管各儿子),孙子进程是一个孤儿进程,孤儿进程会被操作系统领养然后等它退了回收它。
void start() { for (;;) { //。。。 // version2 多进程 int fd=fork(); if(fd == 0) //child { //关闭不需要的监听文件描述符 close(_listensock); if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养 serverIO(sock); close(sock); exit(0); } //父进程 pid_t ret=waitpid(fd,nullptr,0); if(ret > 0) { logMessage(NORMAL,"waitpid child success"); } }
完整代码
void start() { for (;;) { // 4.获取新链接 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue; } // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock); logMessage(NORMAL, "accpet a new link success,get new sock"); cout << "sock: " << sock << endl; // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! // version2 多进程 int fd=fork(); if(fd == 0) //child { //关闭不需要的监听文件描述符 close(_listensock); if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养 serverIO(sock); close(sock); exit(0); } //父进程 pid_t ret=waitpid(fd,nullptr,0); if(ret > 0) { logMessage(NORMAL,"waitpid child success"); } }
看到现在可以多个用户同时连接了。但是多进程并不是一个好方法,因此子进程要拷贝一份父进程的东西。
上面还需要父进程自己回收子进程的资源太麻烦,我们知道子进程退出并不是默默退出的,它会发17号信号,不过系统默认对这个信号是忽略。这些知识我们在信号哪里说过,不在叙述。
因此这里我们让子进程退出然后资源自动被回收。父进程自己忙自己的事情。
void start() { //子进程退出自动被OS回收 signal(SIGCHLD,SIG_IGN); for (;;) { //。。。 // version2 多进程信号版 int fd=fork(); if(fd == 0) { close(_listensock); serverIO(sock); close(sock); exit(0); } }
这里有个问题,子进程关闭了不用的listensock文件描述符,父进程要不要关闭sock文件描述符?
父进程没关sock文件描述符,客户端关闭后再连接,文件描述符是一直增长的状态。文件描述符终有用完的时候!
所以父进程一定要关闭提供服务的sock文件描述符,虽然父进程关闭sock但它不会造成文件关闭,因为有引用计数,等到引用计数到0的时候这个文件才会真正的关闭!
void start() { //子进程退出自动被OS回收 signal(SIGCHLD,SIG_IGN); for (;;) { // 4.获取新链接 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue; } // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock); logMessage(NORMAL, "accpet a new link success,get new sock"); cout << "sock: " << sock << endl; // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! // version2 多进程信号版 int fd=fork(); if(fd == 0) { close(_listensock); serverIO(sock); close(sock); exit(0); } //细节,子进程关闭父进程的,父进程关闭子进程的 close(sock); }
这里可能会有端口绑定失败,原因在具体谈TCP协议再说!
我们先换个端口用。
现在我们想用线程来解决为多人提供服务。
创建新线程,那主线程和新线程之间多文件描述符的态度是什么?
这个sock文件描述符能不能被新线程看到呢?
能!它们共享同一份资源!这里也不用敢像多进程那样让父子进程关闭对应的文件描述符那样做。它们共享同一份资源!
新线程创建好了,主线程也要回收新线程的资源。以前用的是pthread_join,但是在后面我们学过可以使用pthread_deatch进行线程分离,主线程就不用等了。
剩下线程代码细节我们以前说过,这里不再细说。
class tcpServer;//声明 struct ThreadDate { ThreadDate(int sock,tcpServer* tps) :_sock(sock),_tps(tps) {} int _sock; tcpServer* _tps; }; class tcpServer { public: { //。。/ static void* start_routine(void* args) { ThreadDate* td=static_cast<ThreadDate*>(args); pthread_detach(pthread_self());//退出自动回收资源 td->_tps->serverIO(td->_sock); close(td->_sock); delete td; td=nullptr; } void start() { for (;;) { // 4.获取新链接 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue; } // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock); logMessage(NORMAL, "accpet a new link success,get new sock"); cout << "sock: " << sock << endl; // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! //version3 多线程 pthread_t pid; //把this指针和sock一起传过去,因此写个结构体 ThreadDate* td=new ThreadDate(sock,this); pthread_create(&pid,nullptr,start_routine,td); } } //。。。 }
思路是这样的,未来新连接来了,我们可以把新连接构成一个任务,然后放到线程池里,由线程池来进行统一处理。
线程池我们在 【liunx】线程池+单例模式+STL,智能指针和线程安全+其他常见的各种锁+读者写者问题 这里写过,并且做了封装,因此我们拿过来直接用。
线程封装
#pragma once #include<iostream> #include<string> #include<pthread.h> #include<functional> class Thread { typedef std::function<void*(void*)> func_t; private: //类内成员有隐藏的this指针,不加static就会报错! //但是我们又需要this指针,调用类的成员变量,因此把this传过来 static void* start_routine(void* args) { Thread* _this=static_cast<Thread*>(args);//安全进行类型转换 return _this->_func(_this->_args);//调用回调函数,不这样写也可以再写一个类内函数在调用 } public: Thread() { char namebuffer[64]; snprintf(namebuffer,sizeof namebuffer,"thread-%d",_number++); _name=namebuffer; } //为什么这里参数不放在构造函数 //因为我们等会想线程运行的时候,知道是那个线程在运行把_name也一起传过去 void start(func_t func,void* args) { _func=func; _args=args; //这个函数不认识C++的function类,因此我自己写一个函数 pthread_create(&_tid,nullptr,start_routine,this); } void join() { pthread_join(_tid,nullptr); } std::string threadname() { return _name; } ~Thread() {} private: std::string _name;//线程名 func_t _func;//回调函数 void* _args;//回调函数参数 pthread_t _tid;//线程ID static int _number; }; int Thread::_number=1;
锁封装
#pragma once #include<iostream> #include<pthread.h> class Mutex { public: Mutex(pthread_mutex_t* lock):_lock(lock) { pthread_mutex_init(_lock,nullptr); } void lock() { pthread_mutex_lock(_lock); } void unlock() { pthread_mutex_unlock(_lock); } ~Mutex() { pthread_mutex_destroy(_lock); } private: pthread_mutex_t* _lock; }; class LockGuard { public: LockGuard(pthread_mutex_t* lock):_mutex(lock) { _mutex.lock(); } ~LockGuard() { _mutex.unlock(); } private: Mutex _mutex; };
任务封装
#pragma once #include <iostream> #include <functional> #include <string> #include <unistd.h> #include "logMessage.hpp" using namespace std; void serverIO(int sock) { char buffer[1024]; while (true) { // 读 ssize_t n = read(sock, buffer, sizeof(buffer)); if (n > 0) { buffer[n] = 0; cout << "recv message: " << buffer << endl; // 写 string outbuffer = buffer; outbuffer += " server[respond]"; write(sock, outbuffer.c_str(), outbuffer.size()); } else if (n == 0) { // 代表clien退出 logMessage(NORMAL, "client quit, me to!"); break; } } close(sock);//提供完自己关闭文件文件描述符 } class Task { typedef std::function<void(int)> func_t; public: Task(){}; Task(int sock, func_t func) : _sock(sock), _callback(func) { } void operator()() { _callback(_sock); } private: int _sock; func_t _callback; };
线程池单例封装
#pragma once #include "Thread.hpp" #include "Task.hpp" #include <vector> #include <queue> #include "Mutex.hpp" #include <mutex> using namespace std; const int maxcap = 3; // 声明 template <class T> class ThreadPool; template <class T> class ThreadData { public: ThreadData(ThreadPool<T> *poolthis, const string &name) : _poolthis(poolthis), _name_(name) { } ~ThreadData() { } public: ThreadPool<T> *_poolthis; string _name_; }; template <class T> class ThreadPool { private: // 线程调用的处理任务函数 static void *handTask(void *args) { ThreadData<T> *td = static_cast<ThreadData<T> *>(args); while (true) { Task t; // RAII 风格加锁 { // 构造时自动加锁,析构时自动结束 // 局部变量生命周期这个代码块 LockGuard lockguard(td->_poolthis->mutex()); while (td->_poolthis->IsQueueEmpty()) { td->_poolthis->threadwait(); } td->_poolthis->pop(&t); } t(); //执行任务 } delete td; return nullptr; } private: void threadlock() { pthread_mutex_lock(&_lock); } void threadunlock() { pthread_mutex_unlock(&_lock); } void threadwait() { pthread_cond_wait(&_cond, &_lock); } void pop(T *out) { *out = _task_queue.front(); _task_queue.pop(); } bool IsQueueEmpty() { return _task_queue.empty(); } pthread_mutex_t *mutex() { return &_lock; } // 单例不是没有例,构造函数不能去掉,放在private就好了 ThreadPool(int cap = maxcap) : _cap(maxcap) { // 初始化锁,条件变量 pthread_mutex_init(&_lock, nullptr); pthread_cond_init(&_cond, nullptr); // 创建线程 for (int i = 0; i < _cap; ++i) { _threads.push_back(new Thread()); // 创建线程并放在vector里 } } // 去掉赋值,拷贝构造 void operator=(const ThreadPool &) = delete; ThreadPool(const ThreadPool &) = delete; public: // 启动线程 // 在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数 // 而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针 // 因此我们写个类把线程名和this都传过去 void run() { for (auto &thread : _threads) { ThreadData<T> *td = new ThreadData<T>(this, thread->threadname()); thread->start(handTask, td); cout << thread->threadname() << " statr... " << endl; } } // 任务队列放任务 void push(const T &in) { // 保证放任务是安全的,所以先加锁 pthread_mutex_lock(&_lock); _task_queue.push(in); pthread_cond_signal(&_cond); // 队列中有任务就唤醒等待的线程去取任务 pthread_mutex_unlock(&_lock); } ~ThreadPool() { // 销毁锁,条件变量 pthread_mutex_destroy(&_lock); pthread_cond_destroy(&_cond); } // 获取单例 // 成员函数可以调用静态成员和静态成员函数,反之不行 static ThreadPool<T> *getInstance() { // 虽然没有并发问题了,但是还有一个小问题 // 未来每一个线程进来都要lock,unlock // 因此在外面再加一个if判断,未来只要第一次实例化之后就不需要再加锁解锁了 // 大家就可以并发了 if (tp == nullptr) { _singlock.lock(); if (tp == nullptr) { tp = new ThreadPool<T>(); } _singlock.unlock(); } return tp; } private: int _cap; // 线程个数 vector<Thread *> _threads; // 线程放在vector里进行管理 queue<T> _task_queue; // 任务队列 pthread_mutex_t _lock; pthread_cond_t _cond; static ThreadPool<T> *tp; // c++11的锁 static std::mutex _singlock; }; template <class T> ThreadPool<T> *ThreadPool<T>::tp = nullptr; template <class T> mutex ThreadPool<T>::_singlock;
Server 线程池版
void start() { //线程池启动 ThreadPool<Task>::getInstance()->run(); logMessage(NORMAL, "Thread init success"); for (;;) { // 4.获取新链接 struct sockaddr_in peer; socklen_t len = (sizeof(peer)); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符 if (sock < 0) { logMessage(ERROR, "accpet error"); continue; } // logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock); logMessage(NORMAL, "accpet a new link success,get new sock"); cout << "sock: " << sock << endl; // 5.通信 这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作! //version4 线程池 //放任务 ThreadPool<Task>::getInstance()->push(Task(sock,serverIO)); } }
前面我们只是把日志函数简单说了一下,现在加一些设计。
我们想用一下可变参数,未来调用这个函数的时候是准备像下面这样调用。
创建套接字成功,然后打印一下。就像printf函数一样,【日志等级】【时间戳】【pid】【格式化的消息】
// void logMessage(int level,const std::string& message)
// {
// //[日志等级] [时间戳/时间] [pid] [message]
// //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
// std::cout<<message<<std::endl;
// };
void logMessage(int level,const char* format,...)
{
//[日志等级] [时间戳/时间] [pid] [message]
//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
}
如果自己写比较麻烦
// void logMessage(DEBUG, "hello %f, %d, %c", 3.14, 10, 'C'); void logMessage(int level, const char *format, ...) { // [日志等级] [时间戳/时间] [pid] [messge] // [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败] va_list start; //start是一个指针 va_start(start);//让start指向可变参数列表第一个参数 while(*p){//p指向format的位置,如h switch(*p) { case '%': p++; if(*p == 'f') arg = va_arg(start, float);//让start提取一个float类型 ... } } va_end(start); //start指针变成nullptr }
提可变参数列表参数,一般用下面的函数。
const char* level_to_string(int level) { switch(level) { case DUGNUM: return "DUGNUM"; case NORMAL: return "NORMAL"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; } } //时间戳变成时间 char* timeChange() { time_t now=time(nullptr); struct tm* local_time; local_time=localtime(&now); static char time_str[1024]; snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\ local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \ local_time->tm_min, local_time->tm_sec); return time_str; } void logMessage(int level,const char* format,...) { //[日志等级] [时间戳/时间] [pid] [message] //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败] #define NUM 1024 //获取时间 char* nowtime=timeChange(); char logprefix[NUM]; snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid()); // char logconten[NUM]; va_list arg; va_start(arg,format); vsnprintf(logconten,sizeof logconten,format,arg); std::cout<<logprefix<<logconten<<std::endl }
我们也可以把这些日志信息放到文件中去,这里我们使用C++对文件操作
#pragma once #include<iostream> #include<string> #include<stdio.h> #include <cstdarg> #include<ctime> #include<sys/types.h> #include<unistd.h> #include<fstream> #define DUGNUM 0 #define NORMAL 1 #define WARNING 2 #define ERROR 3 #define FATAL 4 #define LOG_NORMAL "log.txt" #define LOG_ERR "log.error" const char* level_to_string(int level) { switch(level) { case DUGNUM: return "DUGNUM"; case NORMAL: return "NORMAL"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; } } //时间戳变成时间 char* timeChange() { time_t now=time(nullptr); struct tm* local_time; local_time=localtime(&now); static char time_str[1024]; snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\ local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \ local_time->tm_min, local_time->tm_sec); return time_str; } void logMessage(int level,const char* format,...) { //[日志等级] [时间戳/时间] [pid] [message] //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败] #define NUM 1024 //获取时间 char* nowtime=timeChange(); char logprefix[NUM]; snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid()); // char logconten[NUM]; va_list arg; va_start(arg,format); vsnprintf(logconten,sizeof logconten,format,arg); //写到文件 if(level == DUGNUM || level == NORMAL || level == WARNING) { std::ofstream oss1(LOG_NORMAL,std::ios_base::out|std::ios_base::app); oss1<<logprefix<<logconten<<std::endl; } else { std::ofstream oss2(LOG_ERR,std::ios_base::out|std::ios_base::app); oss2<<logprefix<<logconten<<std::endl; } };
服务器启动没问题,但是我们把这个终端关掉,此时我们看到服务就没了。也就是说服务器启动了不能关闭xshell,一关就没了。
正常服务器肯定不是这样运行的。服务器启动之后不再受用户登录注销的影响,而服务器可以自由运行的,除非未来不想用它,Quit它。
在liunx中这种进程,叫做守护进程!
我们xshell客户端连上远端的与服务器会有一个会话,会话内部会给我提供一个前台进程bash,然后用户在命令行中自由的启动前台或者后台的任务,在这个会话中,只允许一个前台任务,和0个或者多个后台任务。
后面加& ,将任务放到后台
这里打印出来的东西暂时不用管
然后我们在以后台方式启动几个任务
这就是对应的两个作业,作用编号1、2
然后我们查看当前进程sleep,可以看到PGID,前三个进程是一样的,后三个进程是一样的,并且都是第一个进程的PID,这里想表达的是它们分别属于不同进程组。相同PGID的是一个进程组,组长是第一个进程。然后每个组三个人合起来成为一个进程组干一个作业。
这里想说的是,任务(1、2、3)是由各个进程组来完成的。
这些后端任务都属于同一个会话,从SID全都是一样可以看到。会话是以bashID来命名这个会话的。
& 以后端方式起任务
jobs //查看当前会话
fg 2 //2号任务放前台
ctrl+z //暂停这个任务
一个任务在前台暂停了,立马会被放到后台
然后ls发现bash又回来了,这证明了有且只有一个前台任务。
把一个任务放前台,bash自动变后台,这也说明以前我们自己在以./ 启动任务,是把任务放到前台了,所以输入其他指令根本没用
bg 2 //启动2号任务
所以我们得到的结论就是,作业是可以前台转化的
这就是会话进程组作业之间的关系。
xshell登录的时候会建立这么一堆东西,那退出登录呢?
是不是所有任务都会自动清理。
所以我们要想不受用户登录注销的影响,当这个会话要派生任务的时候,我们只把任务放在后台是不够的,我们需要把任务独立出来,让它自成会话,自成进程组,和终端设备无关。
这样的任务以进程方式呈现,我们叫它守护进程!不受用户登录注销的影响,可以一直在进行运行,除非未来不想让它运行了。
那我们现在就来服务器进程守护进程化。
守护进程化有n多种方式,系统提供了一个函数。不过自己我们自己实现一个。
以后也建议用自己的。
一个进行想要自己变成守护进程,一定要调用setsid
谁调用这个函数,谁就自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长。
但是这里调用setsid不能随便调,要求调用setsid的进程不能是进程组组长。
#pragma once #include <signal.h> #include <unistd.h> #include <cstdlib> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> void deamonSelf(const char *curPath = nullptr) { // 1.让调用进程忽略掉异常的信号 // 2.如何让自己不是组长, setsid // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件 // 4.可选: 进程执行路径发送更改 }
比如客户端给服务端发了一个消息,服务端收到消息然后请求完要给客户端回过去,可是正准备写回去客户端奔溃了,那么服务端此时就是像一个以及被关闭的文件描述符写入,这就如同读端关闭,写端再写没用意义,写端会收到SIGPIPE信号退出。
#pragma once #include <signal.h> #include <unistd.h> #include <cstdlib> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> void deamonSelf(const char *curPath = nullptr) { // 1.让调用进程忽略掉异常的信号 signal(SIGPIPE, SIG_IGN); // 2.如何让自己不是组长, setsid if (fork() > 0)//创建子进程,父进程退出 exit(0); //子进程 --守护进程也叫精灵进程,本质就是孤儿进程的一种 pid_t n = setsid(); assert(n != 1); // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件 // 4.可选: 进程执行路径发送更改 }
守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件。
守护进程和显示器和键盘等已经没关系了,它就是一个独立的在后端运行,只有通过网络端口的方式进行访问。默认会打开0,1,2,可以直接close但是特别简单除暴不合理万一有些日志没写到文件中而打印到显示器但是我们关闭了那不就出问题了吗,进程之间挂掉了。因此我们选择重定向。
linux中存在一个特殊的文件,这个文件就像一个黑洞 ,默认处理方式,凡是向这个文件中写入都统统都丢弃掉。你读我也不阻塞你什么也读不到
#pragma once #include <signal.h> #include <unistd.h> #include <cstdlib> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define DEV "/dev/null" void deamonSelf(const char *curPath = nullptr) { // 1.让调用进程忽略掉异常的信号 signal(SIGPIPE, SIG_IGN); // 2.如何让自己不是组长, setsid if (fork() > 0) exit(0); pid_t n = setsid(); assert(n != 1); // 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件 int fd=open(DEV,O_RDWR); if(fd > 0) { //重定向 dup2(fd,0); dup2(fd,1); dup2(fd,2); } else { close(0); close(1); close(2); } // 4.可选: 进程执行路径发送更改 if(curPath) chdir(curPath); }
#include"tcpServer.hpp" #include<memory> #include"daemon.hpp" void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " local_port\n\n"; } // ./tcpserver port int main(int argc,char* argv[]) { if(argc != 2) { Usage(argv[0]); exit(USAGG_ERR); } uint16_t serverport=atoi(argv[1]); unique_ptr<tcpServer> tsv(new tcpServer(serverport)); tsv->initServer(); //守护进程 deamonSelf(); tsv->start(); return 0; }
现在这个服务端进程就变成守护进程了。
调用setsid,自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长
然后客户端随意访问,服务端没有任何反应,在后端自动给我们反应
而且日志信息也打印到对应的文件中
最神奇的是,我们把xshell关掉,还可以连接到这个服务器,这就把端口暴露给外部,自己写的业务别人就可以直接进行返回了。
这个服务器除非自己主动退出!不然一直在后台运行。
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务端:
服务端首先创建套接字,bind绑定ip和port,然后调用listen设置sock为监听状态,一旦调用listen服务器就由关闭状态变成监听状态就允许客户端来连接了,然后调用accept获取连接。在TCP我们有两套文件描述符,一个创建套接字返回上来的listenfd只用来获取新连接,一个accept返回上来connfd是未来IOfd用它作为IO读取。
这里有个细节,accept是获取连接,并不是创建连接,所谓获取连接前提是底层已经帮我创建好了连接,然后在应用层调用accept把连接拿上来,仅此而已。
客户端:
客户端首先创建套接字,然后调用connect发起链接请求,并且在调用connect的时候OS自动帮我们绑定ip和port。
在TCP这里我们采用链接的方案叫做三次握手。
connect是发起三次握手链接请求的,而真正三次握手建立链接是双方的OS自动完成的。
accept是获取链接的,链接建立好了才能获取链接,因此accept并不参与三次握手的任何细节。
也就是说上层不调用accept,三次握手依旧能完成。
获取链接了,然后客户端和服务端调用read,write等接口进行IO通信,而TCP是可靠性的,所以在发信息后对方会给ACK确认。
TCP保证可靠性和调用read、write没有任何关系,一方发信息对方给ACK确认是双方OS去完成的。 甚至这个发信息也和write和read没关系这个后面说。
曾经建立了连接,才会有未来断开连接。断开连接在TCP这里采用的是四次挥手。
而四次挥手的工作也是由双方OS自动完成的,和我们没用半毛钱关系,而我们决定的是什么时候四次挥手。close是上层调用触发四次挥手。
下面再进一步感性认识三次握手,四次挥手
所谓建立链接是什么?
就如一个男生喜欢一个女生,并不是喜欢他们就在一起了,男生要想和女生在一起就必须先去尝试追求一下。因此男生首先主动发起追求(主动发起连接),他问女生:你愿意做我女朋友吗?女生回答说:好啊,什么时候开始呢? 男生说:就现在把。自此双方三次握手建立成功。
那现实中男生女生在一起了,知道各自是对方男朋友女朋友究竟是在干什么呢?
一定是记下来了一些东西,比如知道他是你的男朋友,她是你的女朋友。所以双方才知道他是我的女朋友,她是我的女朋友。
因此建立链接并不是简单的做了这个动作,它是手段,真正的目的是在双方要各自维护好链接建立好相关的属性信息 。
现在有一个男生有很多女朋友,他要有记录每一个女朋友属性信息。那怎么办呢?
他就需要对每一个女朋友对象先描述,在组织起来。弄一个链接结构管理这些女朋友们。
一个服务器可能有很多客户端发起链接,服务端也需要对这些客户端的链接先描述,在组织!对这些链接用特定的数据结构管理起来。
链接的总结:建立链接是双方OS自动完成的,建立链接过程是双方为了维护链接而创立的内核数据结构,这个内核数据结构对象是要有成本的,这个成本体现在创建的时候要花的时间和空间。
断开链接:是把曾经建立好的链接信息释放掉
断开链接为什么叫四次挥手呢?可以这样理解。男女朋友在一起最后结婚了一起生活了10年,但最终被现实打败了,男生说:我们离婚把。女生说:好啊。然后过了3秒,女生说:你跟我离婚,我也要跟你离婚。男生说:好。这种叫做协商。建立链接是一方主动,所以我们需要三次握手建立链接。断开链接是双方的事情,就必须争得双方的同意。你跟我断开链接,我也要和你断开链接,这叫做协商少了任何一方都只能叫通知。又因为TCP是保证可靠性的,我给你说的话要保证你听到了,你给我说的话要保证我听到了,所以我给你协商时你给我做应答保证我给你做协商时你听到了,反之也一样。所以需要4次,也就是四次挥手。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。