c++实现服务器和多个客户端的实时群聊通信
我们通过TCP/IP来实现多人聊天室,如果租一个服务器我们就可以实现全网的多人聊天室(不懂tcp/ip的点进来https://www.cnblogs.com/yskn/p/9335608.html)!首先我们要了解一下一些知识:
1.socket的IO操作:https://www.cnblogs.com/yskn/p/9355375.html
socket在连接后会有很多的发送和接收的消息如果我们采用阻塞的方式是很不方便的,我们肯本不知道用户会在什么时候进行这些操作,而socket的IO操作就相当于快递站一样加入有这样的消息,就会通知我们来取,在这里我们采用的select方式这种IO方式是通过不断的查询,下面是对select函数的参数解释,我们们可以吧我们关注的对象加入一个数组select会将有消息的自动筛选出来。
我们最主要采用的IO方式以下是对select函数的解释:
int select (
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout
);
第一个参数nfds沒有用,仅仅为与伯克利Socket兼容而提供。
readfds指定一個Socket数组(应该是一个,但这里主要是表现为一个Socket数组),select检查该数组中的所有Socket。如果成功返回,则readfds中存放的是符合‘可读性’条件的数组成员(如缓冲区中有可读的数据)。
writefds指定一个Socket数组,select检查该数组中的所有Socket。如果成功返回,则writefds中存放的是符合‘可写性’条件的数组成员(如连接成功)。
exceptfds指定一个Socket数组,select检查该数组中的所有Socket。如果成功返回,则cxceptfds中存放的是符合‘有异常’条件的数组成员(如连接接失败)。
timeout指定select执行的最长时间,如果在timeout限定的时间内,readfds、writefds、exceptfds中指定的Socket沒有一个符合要求,就返回0。
2.多线程操作:https://www.cnblogs.com/yskn/p/9355556.html
多线程的方式在网络通信中也是十分重要的这样我们才可以同时实现收发,或者是其他的一些功能。
下面就是代码部分绝对好用!
这里我们要注意的是如果我们采用的是阻塞的方式recv,那么一定同步的,假如我们再recv之前就已经send了,那么我们将永远不会受到之前的信息,将会永远的阻塞在这里,所以我建议当我们recv之前发送一个同步信号(自己设置)给服务器,服务器在发送就可以啦
客户端:
.cpp文件
// win_clint.cpp: 定义控制台应用程序的入口点。 // #include "public.h" int main() { client user; user.process(); return 0; } client::client() { user = 0; writing = 0; serverAddr.sin_family = PF_INET; serverAddr.sin_port = SERVER_PORT; serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);//将字符串类型转换uint32_t } void client::init() { int Ret; WSADATA wsaData; // 用于初始化套接字环境 // 初始化WinSock环境 if ((Ret = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) { printf("WSAStartup() failed with error %d ", Ret); WSACleanup(); } user= socket(AF_INET, SOCK_STREAM, 0);//采用ipv4,TCP传输 if (user <= 0) { perror("establish client faild"); printf("Error at socket(): %ld ", WSAGetLastError()); exit(1); }; printf("establish succesfully ");//创建成功 //阻塞式的等待服务器连接 if (connect(user, (const sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { perror("connect to server faild"); printf("Error at socket(): %ld ", WSAGetLastError()); exit(1); } printf("connect IP:%s Port:%d succesfully ", SERVER_IP, SERVER_PORT);//创建成功 } void client::process() { char recvbuf[1024]; fd_set fdread,fedwrite; FD_ZERO(&fdread);//将fds清零 FD_ZERO(&fedwrite);//将fds清零 init(); while (1) { FD_SET(user, &fdread); if(writing==0) FD_SET(user, &fedwrite); struct timeval timeout = { 1,0 };//每个Select等待三秒 switch (select(0, &fdread, &fedwrite, NULL, &timeout)) { case -1: { //perror("select"); printf("Error at socket(): %ld ", WSAGetLastError()); /*exit(1);*/ break; } case 0: { //printf("select timeout......"); break; } default: { if (FD_ISSET(user, &fdread))//则有读事件 { int size = recv(user, recvbuf, sizeof(recvbuf) - 1, 0); if (size > 0) { printf("server:%s ", recvbuf); memset(recvbuf, ' ', sizeof(recvbuf)); } else if (size == 0) { printf("server is closed "); exit(1); } } if (FD_ISSET(user, &fedwrite)) { FD_ZERO(&fedwrite);//将fedwrite清零 writing = 1;//表示正在写作 thread sendtask(bind(&client::sendata, this)); sendtask.detach();//将子线程和主进程分离且互相不影响 } break; } } } } void client::sendata() { char sendbuf[1024]; char middle[1024]; cin.getline(sendbuf, 1024);//读取一行 send(user, sendbuf, sizeof(sendbuf) - 1, 0); writing = 0; }
.h文件
#ifndef PUBLIC_H #define PUBLIC_H //头文件引用 #include<conio.h> #include <iostream> #include <thread> #include <winsock2.h> #include <stdio.h> #include<ws2tcpip.h>//定义socklen_t #pragma comment(lib, "WS2_32") // 链接到WS2_32.lib using namespace std; #define SERVER_IP "127.0.0.1"// 默认服务器端IP地址 #define SERVER_PORT 8888// 服务器端口号 class client { public: client(); void init(); void process(); private: int user; int writing; sockaddr_in serverAddr;//IPV4的地址方式包括服务端地址族、服务端IP地址、服务端端口号 void sendata(); }; #endif // !PUBLIC_H
服务端:
.cpp
#include "public.h" int main() { server ser; ser.process(); return 0; } server::server() { listener = 0; serverAddr.sin_family = PF_INET; serverAddr.sin_port = SERVER_PORT; serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);//将字符串类型转换uint32_t } //初始化函数,功能创建监听套接字,绑定端口,并进行监听 void server::init() { int Ret; WSADATA wsaData; // 用于初始化套接字环境 // 初始化WinSock环境 if ((Ret = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) { printf("WSAStartup() failed with error %d ", Ret); WSACleanup(); } listener = socket(AF_INET, SOCK_STREAM, 0);//采用ipv4,TCP传输 if (listener == -1) { printf("Error at socket(): %ld ", WSAGetLastError()); perror("listener failed"); exit(1); } printf("创建成功 "); unsigned long ul = 1; if (ioctlsocket(listener, FIONBIO, (unsigned long*)&ul) == -1) { perror("ioctl failed"); exit(1); }; /////////////////////////////////////////////////////////////////// //中间的参数绑定的地址如果是IPV4则是/////////////////// //struct sockaddr_in { // sa_family_t sin_family; /* address family: AF_INET */ // in_port_t sin_port; /* port in network byte order */ // struct in_addr sin_addr; /* internet address */ //}; //Internet address. //struct in_addr { // uint32_t s_addr; /* address in network byte order */ //} ///////////////////////////////////////////////////////////////// if (bind(listener, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { perror("bind error"); exit(1); } if (listen(listener, 6) < 0) { perror("listen failed"); exit(1); }; socnum.push_back(listener);//将监听套接字加入 } void server::process() { int mount = 0; fd_set fds; FD_ZERO(&fds);//将fds清零 init(); //下面就是不断的检查 printf("正在等待中 "); while (1) { mount= socnum.size(); //将fds每次都重新赋值 for (int i = 0; i<mount; ++i) { FD_SET(socnum[i], &fds); } struct timeval timeout = { 1,0 };//每个Select等待三秒 switch (select(0, &fds, NULL, NULL, &timeout)) { case -1: { perror("select "); printf("Error at socket(): %ld ", WSAGetLastError()); printf("%d ",mount); /* for (int i = 0; i < mount; ++i) { printf("%d ", socnum[i]); }*/ Sleep(1000); break; } case 0: { //printf("select timeout......"); break; } default: { //将数组中的每一个套接字都和剩余的额套接字进行比较得到当前的任务 for (int i = 0; i < mount; ++i) { //如果第一个套接字可读的消息。就要建立连接 if (i == 0 && FD_ISSET(socnum[i], &fds)) { struct sockaddr_in client_address; socklen_t client_addrLength = sizeof(struct sockaddr_in); //返回一个用户的套接字 int clientfd = accept(listener, (struct sockaddr*)&client_address, &client_addrLength); //添加用户,服务器上显示消息,并通知用户连接成功 socnum.push_back(clientfd); printf("connect sucessfully "); char ID[1024]; sprintf(ID,"You ID is:%d", clientfd); char buf[30]="welcome to yskn's chatroom "; strcat(ID,buf); send(clientfd, ID, sizeof(ID) - 1, 0);//减去最后一个'/0' } if (i != 0 && FD_ISSET(socnum[i], &fds)) { char buf[1024]; memset(buf, ' ', sizeof(buf)); int size = recv(socnum[i], buf, sizeof(buf) - 1, 0); //检测是否断线 if (size == 0 || size == -1) { printf("remote client close,size is%d ", size); //closesocket(socnum[i]);//先关闭这个套接字 FD_CLR(socnum[i], &fds);//在列表列表中删除 socnum.erase(socnum.begin()+i);//在vector数组中删除 } //若是没有掉线 else { printf("clint %d says: %s ", socnum[i], buf); //发送给每个用户 for (int j = 1; j < mount; j++) { char client[1024]; sprintf(client,"client %d:", socnum[i]); strcat(client, buf); send(socnum[j], client, sizeof(client) - 1, 0);//如果 } } } } break; } } } }
.h文件
#ifndef PUBLIC_H #define PUBLIC_H //头文件引用 #include <winsock2.h> #include <stdio.h> #include <vector> #include<ws2tcpip.h>//定义socklen_t using namespace std; #pragma comment(lib, "WS2_32") // 链接到WS2_32.lib #define SERVER_IP "127.0.0.1"// 默认服务器端IP地址 #define SERVER_PORT 8888// 服务器端口号 class server { public: server(); void init(); void process(); private: int listener;//监听套接字 sockaddr_in serverAddr;//IPV4的地址方式 vector <int> socnum;//存放创建的套接字 }; #endif // !PUBLIC_H