两台主机之间通信的目的不仅仅是为了将数据发送给对端主机,而是为了访问对端主机上的某个服务。端口就是找到这个服务的钥匙,标识主机上的一个进程。
网络通信的本质:本质上就是一种进程间通信
通过IP地址和MAC地址能够将数据发送到对端主机了,但实际我们是想将数据发送给对端主机上的某个服务进程。
此外,数据的发送者也不是主机,而是主机上的某个进程,比如当我们用浏览器访问数据时,实际就是浏览器进程向对端服务进程发起的请求。
socket通信的本质就是一种进程间通信。
端口号(port)是传输层协议的内容.
因为端口号是隶属于某台主机的,所以端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复。此外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。
端口号和进程ID都可以标识一个进程。为什么网络通信不直接使用进程ID?
进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。
一台机器上可能会有大量的进程,但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了。
Port和进程ID反映了一个进程使用的不同场景,在同一主机下使用进程ID标识一个进程,在网络通信中使用Port标识一个进程
OS如何通过Port找到进程ID
实际底层采用了Hash方式,建立了进程ID和Port的映射关系。当底层拿到Port后可以通过Hash映射直接找到对应的进程。
TCP协议
TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。
特点
UDP协议
UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。
使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
特点
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。
只在本地机器上运行,那么是不需要考虑大小端问题的。不同的主机存放的方式可能不同,如果不同一标准,那么两台主机通信数据就是错误混乱的。
网络数据流地址
网络字节序与主机字节序之间的转换
#include
uint32_t htonl(uint32_t hostlong);
主机字节序转换为网络字节序【大端】----转换4字节的IP地址
uint16_t htons(uint16_t hostshort);
主机字节序转换为网络字节序----转换2字节的端口号
uint32_t ntohl(uint32_t netlong);
网络字节序【大端】转换为主机字节序 -----转换4字节的IP地址
uint16_t ntohs(uint16_t netshort);
网络字节序【大端】转换为主机字节序 -----转换2字节的端口号
常见接口
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);参数说明:domain:IP地址类型常用:AF_INET,AF_INET6type:套接字类型SOCK_STREAM:它提供基于字节流的有序、可靠、双向连接
可以支持带外数据传输机制。【流式套接字------用于TCP通信】SOCK_DGRAM:支持数据报(固定最大值的无连接、不可靠消息长度)。【报文套接字----UDP通信】SOCK_SEQPACKETprotocol:默认0返回值:成功:返回文件描述符失败:-1
绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t addrlen);参数:sockfd:套接字address:ip套接字结构体地址addrlen:结构体大小返回值:成功返回0失败返回-1
开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);参数:socket:套接字backlog:已完成连接队列和未完成连接队列数之和的最大值 128。一般这个参数为5
接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);参数:socket:套接字address:传入类型参数,获取客户端的IP和端口信息address_len:结构体大小的地址
返回值:新的已连接套接字的文件描述符
建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);参数sockfd:sockfd套接字【文件描述符】addr:套接字结构体的地址addrlen:结构体的长度
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行跨网络通信时我们需要传递的端口号和IP地址,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6 ,然而, 各种网络协议的地址格式并不相同,为了让套接字编程具有同一的接口,于是出现了sockaddr结构体。
创建套接字操作系统做了什么?
int socket(int domain, int type, int protocol);
当我们调用socket函数创建一个套接字时,实际上相当于我们打开了一个网络文件。打开文件后,在内核层面上,在内核管理文件结构体的FCB链表中链入了一个struct file结构体。并将结构体地址填入到对应的fd_array[ ]中,返回对应的数组下标。这个数组下标也就是socket的返回值
下面验证上面的说法:我们关闭标准错误,然后再创建套接字,观察返回值是否为2
int main(){close(2);int sockfd=socket(AF_INET,SOCK_DGRAM,0);if(sockfd<0){std::cout<<"socket err "<
每一个struct file结构体包含了对应打开文件的各种信息,比如文件属性、操作方法以及文件缓冲区等。
如何理解bind?
在创建套接字后,OS如何确定对于的文件描述符sockfd对应的是一个磁盘文件还是网络文化。
在bind()接口中,struct sockaddr *address 参数包含了IP和端口号信息。而bind,需要将IP地址和port端口号告诉对应的网络文件;
此时可以改变网络文件当中,struct file_operations当中文件操作函数的指向,把对应的操作函数改为对应网卡的操作函数。
所以bind就是将文件和网络联系起来。(通过修改strucqt file_operations中函数指针的指向)
IP地址的表现形式有两种:
192.168.233.123
这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址。关于字符串IP和整形IP的转化,在下面的博客中由详细的说明:
【网络编程】套接字_影中人lx的博客-CSDN博客
如果要想写的服务器在网络上允许,需要绑定服务器的IP地址。由于云服务的IP地址是由厂商提供,这个IP地址并不一定是真正的公网IP。如果需要让外网访问,因此需要绑定0。
系统中提供了一个INADDR_ANY的宏,对应的值为0。
bind() INADDR_ANY的好处
一个服务器的带宽足够大,那么一台服务器的数据接收能力就约束了机器的IO能力。因此一台机器的底层可能装有多个网卡,此时这台服务器可能就有多个IP地址。
但是接收数据的服务器的端口8081只有一个,当网络中有数据时,这台服务器的多张网卡底层都收到了数据。如果服务器只绑定其中的一个IP,那么服务器只能从对应的网卡接收数据。
如果服务器绑定的是INADDR_ANY,那么网卡接收到的数据,服务器端口都可以接收,极大的提高了效率。
在C中,当无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表。例如:
void foo(...);
void foo(int level,char* format...);
函数传参原理
函数参数是以栈的形式,从右到左入栈。
参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个参数开始入栈。因此栈底高地址,栈顶低地址
void func(int x, float y, char z);
那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是 x->y->z。
相关接口
typedef char* va_list;void va_start (va_list ap, prev_param ); /* ANSI version */
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
int demo(const char *msg, ...)
{/*定义保存函数参数的结构*/va_list argp;int argno = 0;char *para;/*argp指向传入的第一个可选参数,msg是最后一个确定的参数*/va_start(argp, msg);while (1){para = va_arg(argp, char *);if (strcmp(para, "") == 0)break;printf("Parameter #%d is: %s\n", argno, para);argno++;}va_end(argp);/*将argp置为NULL*/return 0;
}
int main(void)
{demo("DEMO", "This", "is", "a", "demo!", "");return 0;
}
#include
int vsnprintf(char *str, size_t size, const char *format, va_list ap);参数:str:保存输出字符数组的存储区。size:存储区的大小。format:包含格式字符串的C字符串,其格式字符串与printf中的格式相同arg:变量参数列表,用va_list 定义。第一个参数必须是format
按照一定的格式打印参数,这也是printf函数的底层实现。
int demo(const char *format, ...)
{/*定义保存函数参数的结构*/va_list argp;va_start(argp,format);char buff[128];vsnprintf(buff,sizeof(buff)-1,format,argp);va_end(argp);std::cout<<"buff : "<const char* format="Demo :%s,int:%d,double:%f,char:%c";demo(format,"DEMO",2022,11.11,'a');return 0;
}
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level,const char*format,...){assert(level>=DEBUG);assert(level<=FATAL);char* name=getenv("USER");char loginfo[1024];va_list ap;va_start(ap,format);vsnprintf(loginfo,sizeof(loginfo)-1,format,ap);va_end(ap);//如果是FATAL错误,输出到标准错误中FILE* out=(level==FATAL)?stderr:stdout;fprintf(out, "%s | %u | %s | %s\n", \log_level[level], \(unsigned int)time(nullptr),\name == nullptr ? "unknow":name,\loginfo);
}
static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}class Udpserver
{
public:Udpserver(int port,std::string ip=""):port_((uint16_t)port),ip_(ip),sockfd_(-1){}~Udpserver(){}//初始化接口void init(){sockfd_=socket(AF_INET,SOCK_DGRAM,0);if(sockfd_<0){logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);exit(1);}logMessage(DEBUG,"create socket sucess: %d",sockfd_);//绑定端口struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(port_);// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n//绑定任意端口local.sin_addr.s_addr=htonl(INADDR_ANY);int ret=bind(sockfd_,(const struct sockaddr*)&local,sizeof(local));if(ret<0){logMessage(FATAL,"bind:%s:%d",strerror(errno),sockfd_);exit(2);}logMessage(DEBUG,"bind sucess:%d",sockfd_);}void checkOnlineUser(std::string& clientip,uint32_t clientport,struct sockaddr_in& client){std::string key = clientip;key += ":";key += std::to_string(clientport);auto iter=users.find(key);if(iter==users.end()){users.insert({key, client});}else{//do nothing}}//接收消息,实现广播void start(){//发送缓存和接收缓存char recvbuff[1024]={0};char sendbuff[1024]={0};while(true){struct sockaddr_in client;socklen_t len=sizeof(client);ssize_t s=recvfrom(sockfd_,recvbuff,sizeof(recvbuff)-1,0,(struct sockaddr*)&client,&len);if(s>0){recvbuff[s]=0;}else if(s==-1){logMessage(WARINING,"recvform:%s:%d",strerror(errno),sockfd_);continue;}// 读取成功的,除了读取到对方的数据,还要读取到对方的网络地址[ip:port]std::string clientIp = inet_ntoa(client.sin_addr); //拿到了对方的IPuint32_t clientPort = ntohs(client.sin_port); // 拿到了对方的portcheckOnlineUser(clientIp, clientPort, client); //如果存在,什么都不做,如果不存在,就添加// 打印出来客户端给服务器发送过来的消息logMessage(NOTICE, "[%s:%d]# %s", clientIp.c_str(), clientPort, recvbuff);//实现广播messageRoute(clientIp,clientPort,recvbuff);}}void messageRoute(std::string ip, uint32_t port, std::string info){std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;for(auto &user : users){sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));}}
private:int sockfd_; //套接字uint16_t port_;std::string ip_;std::unordered_map users; //记录在线用户
};
//多线程客户端,一个线程用于用户IO,一个线程用于接收广播消息struct sockaddr_in server; //服务器信息static void Usage(std::string name){std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}void* recv_rounite(void* arg){while (true){int sockfd=*(int*)arg;char buff[1024];struct sockaddr_in client;socklen_t len=sizeof(client);ssize_t s=recvfrom(sockfd,buff,sizeof(buff),0,(struct sockaddr*)&client,&len);if (s > 0){buff[s] = 0;std::cout << "server echo# " << buff << std::endl;}}}
int main(int argc,char*argv[]){if(argc!=3){Usage(argv[0]);exit(1);}std::string server_ip=argv[1];uint16_t server_port=atoi(argv[2]); int sockfd=socket(AF_INET,SOCK_DGRAM,0);assert(sockfd>0);bzero(&server,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(server_port);server.sin_addr.s_addr=inet_addr(server_ip.c_str());//接收消息pthread_t rev;pthread_create(&rev,nullptr,recv_rounite,(void*)&sockfd);std::string buffer;//用户进行IO操作while(true){std::cerr<<"please Enter# ";std::getline(std::cin,buffer);sendto(sockfd,buffer.c_str(),buffer.size(),0,(const struct sockaddr*)&server,sizeof(server));}close(sockfd);return 0;
}
为什么在UDP中客户端中,用户不需要进行bind?
其实需要bind。但是不是由用户进行bind,而是OS自动进行bind。
**严重不推荐自己在客户端绑定:**client可能有很多,不能给客户端bind指定的port,port可能被别的client使用了,如果再进行bind就会发生崩溃。
结果展示
这个简单的服务器也支持多个用户同时在线。
客户端主要实现连接客户端。与用户IO交互,接收用户的命令发送给客户端。并接收客户端发生的信息。
///多线程客户端,一个线程用于用户IO,一个线程用于接收广播消息struct sockaddr_in server; //服务器信息
volatile bool quit=false; //判断用户是否需要退出static void Usage(std::string name){std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}void* recv_rounite(void* arg){while (true){int sockfd=*(int*)arg;char buff[1024];struct sockaddr_in client;socklen_t len=sizeof(client);ssize_t s=read(sockfd,buff, sizeof(buff) - 1);if (s > 0){buff[s] = 0;std::cout << "server echo# " << buff << std::endl;}}}
int main(int argc,char*argv[]){if(argc!=3){Usage(argv[0]);exit(1);}std::string server_ip=argv[1];uint16_t server_port=atoi(argv[2]); int sockfd=socket(AF_INET,SOCK_STREAM,0);if (sockfd< 0){std::cerr << "socket: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}bzero(&server,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(server_port);server.sin_addr.s_addr=inet_addr(server_ip.c_str());socklen_t len=sizeof(server);int ret=connect(sockfd,(const sockaddr*)&server,len);//接收消息pthread_t rev;pthread_create(&rev,nullptr,recv_rounite,(void*)&sockfd);string buffer;//用户进行IO操作while(true){std::cerr<<"please Enter# ";std::getline(std::cin,buffer);//判断用户是否需要退出if(strcasecmp(buffer.c_str(),"quit")==0){quit=true;}ssize_t s=write(sockfd,buffer.c_str(),sizeof(buffer.c_str())-1);if(s>0){continue;}else if(s<=0){break;}}close(sockfd);return 0;
}
TCP服务端和UDP服务端的写法不同,TCP是有连接的。需要经过下面的步骤:
服务器框架
class Tcpserver
{
public:Tcpserver(uint16_t port, const std::string ip = ""): port_((uint16_t)port), ip_(ip), listen_sockfd_(-1){pthread_mutex_init(&_mutex,nullptr);}~Tcpserver(){}void init(){//创建监听套接字listen_sockfd_=socket(PF_INET, SOCK_STREAM, 0);/*socket日志信息*///绑定端口bind(listen_sockfd_, (const struct sockaddr *)&local, sizeof(local));/*bind日志信息*///监听套接字,至于为什么是5在后面的章节解释listen(listen_sockfd_, 5);/*listen日志信息*///循环提取连接while (true){int sock = accept(listen_sockfd_, (sockaddr *)&client, &len);/*accept日志信息*///处理事务}/*事务处理*/}
private:int listen_sockfd_; //套接字uint16_t port_;std::string ip_;std::unordered_map users; //记录在线用户std::unordered_mapusers_sockfd;//用户信息与套接字的映射关系。pthread_mutex_t _mutex; //访问临界资源的锁
};
服务端单执行流处理方式
如果服务端采用单执行流的方式提取连接,并与客户端进行网络通信。由于服务端存在读取客户端信息的行为,且该行为是一个阻塞行为;因此服务器无法达到一次处理多个客户请求的需求。
多执行流的处理方式
6.1多进程方式+信号捕捉SIGCHLD
采用多进程的方式处理多个执行流,存在回收子进程的问题;如果使用waitpid等待子进程,则会发生阻塞,无法做到同时与多个客户通信。
根据信号一节的知识我们知道,子进程在退出后会向父进程发送SIGCHLD信号,所以考虑以捕捉信号的方式回收子进程。
static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}class Tcpserver
{
public:Tcpserver(uint16_t port, const std::string ip = ""): port_((uint16_t)port), ip_(ip), listen_sockfd_(-1){pthread_mutex_init(&_mutex,nullptr);}~Tcpserver(){}//初始化接口void init(){listen_sockfd_ = socket(PF_INET, SOCK_STREAM, 0);if (listen_sockfd_ < 0){logMessage(FATAL, "socket:%s:%d", strerror(errno), listen_sockfd_);exit(1);}logMessage(DEBUG, "create socket sucess: %d", listen_sockfd_);//绑定端口struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = PF_INET;local.sin_port = htons(port_);// 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip// INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n//绑定任意端口ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));int ret = bind(listen_sockfd_, (const struct sockaddr *)&local, sizeof(local));if (ret < 0){logMessage(FATAL, "bind:%s", strerror(errno));exit(2);}logMessage(DEBUG, "bind sucess:%d", listen_sockfd_);//监听int res = listen(listen_sockfd_, 5);if (res < 0){logMessage(FATAL, "listen:%s", strerror(errno));exit(3);}logMessage(DEBUG, "listen sucess:%d", listen_sockfd_);}//检测用户是否已经被添加void checkOnlineUser(std::string &clientip, uint32_t clientport, struct sockaddr_in &client){std::string key = clientip;key += ":";key += std::to_string(clientport);auto iter = users.find(key);if (iter == users.end()){users.insert({key, client});}else{// do nothing}}//接收消息,实现广播void start(){//注册信号捕捉signal(SIGCHLD,SIG_IGN);//发送缓存和接收缓存char recvbuff[1024] = {0};char sendbuff[1024] = {0};while (true){//提取连接struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listen_sockfd_, (sockaddr *)&client, &len);if(sock<0){logMessage(WARINING,"accept:%s[%d]",strerror(errno),sock);}logMessage(DEBUG,"accept success: [%d]",sock);//获取客户端的IP和PORT信息std::string clientIp = inet_ntoa(client.sin_addr);uint32_t clientport=ntohs(client.sin_port);//添加客户与套接字中间的映射users_sockfd[clientIp]=sock;pid_t pid=fork();if(pid==0){ //子进程与客户端进行通信//子进程再添加一次,父子进程会发生写时拷贝users_sockfd[clientIp]=sock;close(listen_sockfd_);checkOnlineUser(clientIp,clientport,client);//与客户端进行通信info_to_client(clientIp,clientport,sock);exit(0);}else{ //父进程需要关闭子进程通信的套接字,继续提取链接close(sock);continue;}}}//实现广播void messageRoute(std::string ip, uint32_t port, std::string info){std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;for(auto& sockfd:users_sockfd){write(sockfd.second,message.c_str(),strlen(message.c_str()));}}void info_to_client(std::string& clientip,uint32_t clientport,int sock){assert(sock>=0);assert(!clientip.empty());assert(clientport>=1024);char buff[1024]; logMessage(DEBUG,"begin..................%s[%d]",clientip.c_str(),sock);while (true){ssize_t s=read(sock,buff,sizeof(buff)-1);if(s>0){//判断用户是否需要退出buff[s]='\0';if(strcasecmp(buff,"quit")==0){logMessage(DEBUG,"client quit---%s[%d]",clientip.c_str(),sock);break;}logMessage(DEBUG,"%s[%d] information :%s",clientip.c_str(),sock,buff);//实现广播messageRoute(clientip,clientport,buff);}else if(s==0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端返回s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG,"client quit-----%s[%d]",clientip.c_str(),sock);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientip.c_str(), clientport, strerror(errno));break;}}logMessage(DEBUG,"end..................%s[%d]",clientip.c_str(),sock);close(sock);logMessage(DEBUG, "server close %d done", sock);}
private:int listen_sockfd_; //套接字uint16_t port_;std::string ip_;std::unordered_map users; //记录在线用户std::unordered_mapusers_sockfd;pthread_mutex_t _mutex;
};int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(3);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}Tcpserver svr(port, ip);svr.init();svr.start();return 0;
}
接下来用两个客户端向服务器发送信息。
我们明明实现了广播的功能,为什么客户端之间无法接收到对方的消息?
6.2.多进程方式+阻塞等待:子进程再创建子进程
孤儿进程会被系统进程领养,回收的问题就交给了系统来回收。因此上面的程序只有start()函数需要修改,其他不变。
void start()
{//注册信号捕捉signal(SIGCHLD, SIG_IGN);//发送缓存和接收缓存char recvbuff[1024] = {0};char sendbuff[1024] = {0};while (true){//提取连接struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listen_sockfd_, (sockaddr *)&client, &len);if (sock < 0){logMessage(WARINING, "accept:%s[%d]", strerror(errno), sock);}logMessage(DEBUG, "accept success: [%d]", sock);//获取客户端的IP和PORT信息std::string clientIp = inet_ntoa(client.sin_addr);uint32_t clientport = ntohs(client.sin_port);//添加客户与套接字中间的映射users_sockfd[clientIp] = sock;pid_t pid = fork();if (pid == 0){ //子进程close(listen_sockfd_);//孙子进程实现业务逻辑,子进程负责创建孙子进程//子进程if (fork() > 0){exit(0);}//孙子进程,与客户端进行通信info_to_client(clientIp, clientport, sock);exit(0);}//父进程回收子进程close(sock);pid_t ret = waitpid(pid, nullptr, 0); //阻塞方式回收子进程}
}
各个线程共享同一个文件描述符:所以当主线程accept到一个文件描述符时,其他线程可以直接访问到这个文件描述符的。
虽然其他线程可以直接访问文件描述符,但是其他线程不知道它所服务的客户端对应的时哪个文件描述符,因此主线程创建次线程后需要告诉新线程对应应该访问的文件描述符的值。
//线程的参数列表
class arglist
{
public:struct sockaddr_in *_addr;int _sockfd;Tcpserver *_svr;//构造函数arglist(int sockfd, Tcpserver *svr, struct sockaddr_in *addr): _sockfd(sockfd), _svr(svr), _addr(addr){}
};void start()
{//注册信号捕捉signal(SIGCHLD, SIG_IGN);//发送缓存和接收缓存char recvbuff[1024] = {0};char sendbuff[1024] = {0};while (true){//提取连接struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listen_sockfd_, (sockaddr *)&client, &len);if (sock < 0){logMessage(WARINING, "accept:%s[%d]", strerror(errno), sock);}logMessage(DEBUG, "accept success: [%d]", sock);//获取客户端的IP和PORT信息std::string clientIp = inet_ntoa(client.sin_addr);uint32_t clientport = ntohs(client.sin_port);//添加客户与套接字中间的映射users_sockfd[clientIp] = sock;//多线程版本arglist *arg = new arglist(sock, this, &client);users_sockfd[clientIp] = sock;//创建一个线程,用于对该套接字的运行pthread_t pid;pthread_create(&pid, nullptr, pthread_run, (void *)arg);}
}//静态函数内部,必须是某个对象调用具体的成员函数;
static void *pthread_run(void *arg)
{pthread_detach(pthread_self());arglist *arl = (arglist *)arg;//注册用户struct sockaddr_in *client = arl->_addr;socklen_t len = sizeof(client);// 读取成功的,除了读取到对方的数据,还要读取到对方的网络地址[ip:port]std::string clientIp = inet_ntoa((*client).sin_addr); //拿到了对方的IPuint32_t clientPort = ntohs((*client).sin_port); // 拿到了对方的portarl->_svr->lock();arl->_svr->checkOnlineUser(clientIp, clientPort, *client); //如果存在,什么都不做,如果不存在,就添加arl->_svr->unlock();//循环接收数据char recvbuff[1024];while (true){bzero(&recvbuff, sizeof(recvbuff));//接收消息ssize_t s = read(arl->_sockfd, recvbuff, sizeof(recvbuff) - 1);// 打印出来客户端给服务器发送过来的消息logMessage(NOTICE, "[%s:%d]# %s", clientIp.c_str(), clientPort, recvbuff);//实现广播arl->_svr->messageRoute(clientIp, clientPort, recvbuff);}
}
线程之间共享进程的数据,因此多线程可以实现广播的功能,下面以两个客户端为例。
来一个连接就创建一个线程,断开一个连接就释放一个线程,这样频繁地创建和释放线程资源,对OS来说是一种负担。线程池可以避免但时间内大量的链接请求,此外还能保证内核被充分利用,防止过分调度。
task.hpp
task类主要负责生产任务,内部定义一个回调函数指针。
class task
{
public:using callback_t = std::function;//构造函数task(std::string clientip, uint32_t clientport,int sock,callback_t func):clientip_(clientip),clientport_(clientport),sock_(sock),func_(func){}task():sock_(-1), clientport_(-1){}void operator()(){logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始...",pthread_self(), clientip_.c_str(), clientport_);func_(clientip_,clientport_,sock_);logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束...",pthread_self(), clientip_.c_str(), clientport_);}private:int sock_;std::string clientip_;uint32_t clientport_;callback_t func_;
};
线程池定义
线程池的实现在另一篇文章中有详细介绍,下面是实现线程池的链接。
线程实现链接:[线程池的实现]((599条消息) 【Linux】线程池_影中人lx的博客-CSDN博客)
tcpserver.cpp
该文件的改动较大,实现广播的函数和实现通信的函数需要定义在类外。定义在类内,默认的第一个参数是this。
服务器运行程序负责不断的生产任务,并向任务队列中添加任务。
//类外定义
std::unordered_map users_sockfd;
//实现广播
void messageRoute(std::string ip, uint32_t port, std::string info)
{std::string message = "[";message += ip;message += ":";message += std::to_string(port);message += "]# ";message += info;for (auto &sockfd : users_sockfd){write(sockfd.second, message.c_str(), strlen(message.c_str()));}
}
void info_to_client(const std::string clientip, uint32_t clientport, int sock)
{assert(sock >= 0);assert(!clientip.empty());assert(clientport >= 1024);char buff[1024];logMessage(DEBUG, "begin..................%s[%d]", clientip.c_str(), sock);while (true){ssize_t s = read(sock, buff, sizeof(buff) - 1);if (s > 0){//判断用户是否需要退出buff[s] = '\0';if (strcasecmp(buff, "quit") == 0){logMessage(DEBUG, "client quit---%s[%d]", clientip.c_str(), sock);break;}logMessage(DEBUG, "%s[%d] information :%s", clientip.c_str(), sock, buff);//实现广播messageRoute(clientip, clientport, buff);}else if (s == 0){// pipe: 读端一直在读,写端不写了,并且关闭了写端,读端返回s == 0,代表对端关闭// s == 0: 代表对方关闭,client 退出logMessage(DEBUG, "client quit-----%s[%d]", clientip.c_str(), sock);break;}else{logMessage(DEBUG, "%s[%d] - read: %s", clientip.c_str(), clientport, strerror(errno));break;}}logMessage(DEBUG, "end..................%s[%d]", clientip.c_str(), sock);close(sock);logMessage(DEBUG, "server close %d done", sock);
}
//类外定义class Tcpserver
{void start(){//注册信号捕捉// signal(SIGCHLD, SIG_IGN);//创建线程池pool_ = threadpool::getInstance();//发送缓存和接收缓存char recvbuff[1024] = {0};char sendbuff[1024] = {0};pool_->start();while (true){//提取连接struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(listen_sockfd_, (sockaddr *)&client, &len);if (sock < 0){logMessage(WARINING, "accept:%s[%d]", strerror(errno), sock);}logMessage(DEBUG, "accept success: [%d]", sock);//获取客户端的IP和PORT信息std::string clientIp = inet_ntoa(client.sin_addr);uint32_t clientport = ntohs(client.sin_port);//添加客户与套接字中间的映射users_sockfd[clientIp] = sock;//创建任务task t(clientIp, clientport, sock, info_to_client);pool_->push(t);}
}
服务器实现结果
源代码地址
socket · 影中人/test - 码云 - 开源中国 (gitee.com)