我們通過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\n", Ret); WSACleanup(); } user= socket(AF_INET, SOCK_STREAM, 0);//采用ipv4,TCP傳輸 if (user <= 0) { perror("establish client faild"); printf("Error at socket(): %ld\n", WSAGetLastError()); exit(1); }; printf("establish succesfully\n");//創建成功 //阻塞式的等待服務器連接 if (connect(user, (const sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { perror("connect to server faild"); printf("Error at socket(): %ld\n", WSAGetLastError()); exit(1); } printf("connect IP:%s Port:%d succesfully\n", 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\n", 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\n", recvbuf); memset(recvbuf, '\0', sizeof(recvbuf)); } else if (size == 0) { printf("server is closed\n"); 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\n", Ret); WSACleanup(); } listener = socket(AF_INET, SOCK_STREAM, 0);//采用ipv4,TCP傳輸 if (listener == -1) { printf("Error at socket(): %ld\n", WSAGetLastError()); perror("listener failed"); exit(1); } printf("創建成功\n"); 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("正在等待中\n"); 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\n"); printf("Error at socket(): %ld\n", WSAGetLastError()); printf("%d\n",mount); /* for (int i = 0; i < mount; ++i) { printf("%d\n", 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\n"); char ID[1024]; sprintf(ID,"You ID is:%d", clientfd); char buf[30]="welcome to yskn's chatroom\n"; strcat(ID,buf); send(clientfd, ID, sizeof(ID) - 1, 0);//減去最后一個'/0' } if (i != 0 && FD_ISSET(socnum[i], &fds)) { char buf[1024]; memset(buf, '\0', sizeof(buf)); int size = recv(socnum[i], buf, sizeof(buf) - 1, 0); //檢測是否斷線 if (size == 0 || size == -1) { printf("remote client close,size is%d\n", size); //closesocket(socnum[i]);//先關閉這個套接字 FD_CLR(socnum[i], &fds);//在列表列表中刪除 socnum.erase(socnum.begin()+i);//在vector數組中刪除 } //若是沒有掉線 else { printf("clint %d says: %s \n", 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