前言:
大二时候完成的网络编程大作业,用C\C++语言写的一款简易聊天室。目前一个服务器只能连两个客户端,两个以上的我搞不出来。也可以在三台电脑操作,只要输入服务器的IP地址就行了。
使用的编译器:VS2010、VS2019
演示一下:
左边的是服务器,中间的是客户端1:zhang san,右边是客户端2:li si。
服务器源码:
#include "WinSock2.h" // winsock2.h是套接字接口。
#include "process.h" //process.h 是包含用于和宏指令的作用声明与螺纹和过程一起使用的C标头文件。
#include "stdio.h" //被包含的文件通常是由系统提供的,其扩展名为.h,而stdio为standard input output的缩写,意为“标准输入输出”
#include "stdlib.h" //stdlib 头文件里包含了C语言的一些函数,该文件包含了的C语言标准库函数的定义
#include "conio.h" //将conio.h包含入你的程序,使你可以引用其中声明的函数。conio.h不是C标准库中的头文件。
#pragma comment(lib,"ws2_32.lib") // ws2_32.lib是套接字实现。
#define SEND_OVER 1 //已经转发消息
#define SEND_YET 0 //还没转发消息
int Status = SEND_YET; //状态设为未发送sockaddr_in ClientAddr = { 0 }; //客户端地址
HANDLE g_hRecv1 = NULL; //handle为句柄,用表示对象 void *g_hRecv1=NULL;
HANDLE g_hRecv2 = NULL;//客户端信息结构体struct Client_inf
{SOCKET sClient; //客户端套接字char buf[512]; //数据缓冲区char userName[20]; //客户端用户名char IP[20]; //客户端IPUINT_PTR flag; //标记客户端,用来区分不同的客户端,typedef unsigned int_w64 UINT_PTR,是为解决32位与64位编译器的兼容性而设置的关键字
};typedef Client_inf Client;Client ClientSock[2] = { 0 }; //创建一个客户端结构体
SOCKET ServerSocket=INVALID_SOCKET;unsigned __stdcall ThreadSend(void* param); //声明发送数据的线程函数
unsigned __stdcall ThreadRecv(void* param); //声明接收数据的线程函数
unsigned __stdcall ThreadAccept(void* param); //声明接受请求的线程函数int main(void)
{int ret,len;WSADATA data ;struct sockaddr_in ServerAddr; //服务端地址unsigned short SERVERPORT = 6666; //服务器监听端口//初始化套接字ret = WSAStartup(MAKEWORD(2,2),&data);//WSAStartup函数的返回值是0表示成功if (SOCKET_ERROR == ret){printf("WSAstartup error!\n");return -1;}//创建套接字ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (INVALID_SOCKET == ServerSocket){printf("创建socket失败!\n");return -2;}//设置服务器地址ServerAddr.sin_family = AF_INET;//连接方式ServerAddr.sin_port = htons(SERVERPORT);//服务器监听端口ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//任何客户端都能连接这个服务器//绑定服务器ret = bind(ServerSocket, (struct sockaddr*)&ServerAddr, sizeof(sockaddr));//bind是一组用于函数绑定的模板。if (SOCKET_ERROR == ret){printf("bind error!\n");closesocket(ServerSocket);WSACleanup();return -3;}//设置监听客户端连接数ret =listen(ServerSocket,2);//listen是创建一个套接口并监听申请的连接.if (SOCKET_ERROR == ret){printf("listen error!\n");closesocket(ServerSocket);WSACleanup();return -4;}printf("开启聊天成功,等待用户连接....\n");_beginthreadex(NULL, 0, ThreadAccept, NULL, 0, 0); //启动接受连接线程int k=0;while(k<100) //让主线程休眠,不让它关闭TCP连接.{Sleep(10000000);k++;}//关闭套接字for (int j = 0;j < 2;j++){if (ClientSock[j].sClient != INVALID_SOCKET)closesocket(ClientSock[j].sClient);}closesocket(ServerSocket);WSACleanup();return 0;
}//发送数据线程函数的定义
unsigned __stdcall ThreadSend(void* param) //param形参
{int ret = 0;int flag = *(int*)param; //int*是把param从void*强制转化为int*,要不然取值的时候系统会不知所措,外面的*就是取值操作,取param这个地址里面保存的整型值SOCKET client = INVALID_SOCKET; //创建一个临时套接字来存放要转发的客户端套接字char temp[512] = { 0 }; //创建一个临时的数据缓冲区,用来存放接收到的数据memcpy(temp, ClientSock[!flag].buf, sizeof(temp)); //拷贝sprintf( ClientSock[flag].buf, "%s: %s", ClientSock[!flag].userName, temp);//添加一个用户名头if (strlen(temp) != 0 && Status == SEND_YET&&temp!="\n"&&(*temp!=' ')) //如果数据不为空且还没转发则转发{ret = send( ClientSock[flag].sClient, ClientSock[flag].buf, sizeof( ClientSock[flag].buf), 0);}//send()是一个计算机函数,功能是向一个已经连接的socket发送数据,如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。if (SOCKET_ERROR == ret){ return 1;}Status = SEND_OVER; //转发成功后设置状态为已转发return 0;
}//接受数据线程函数的定义
unsigned __stdcall ThreadRecv(void* param)
{SOCKET client = INVALID_SOCKET; //客户端套接字int flag = 0; if (*(int*)param == ClientSock[0].flag) //判断是哪个客户端发来的消息{client = ClientSock[0].sClient;flag = 0;} else if (*(int*)param == ClientSock[1].flag){client = ClientSock[1].sClient;flag = 1;}char temp[512] = { 0 }; //临时数据缓冲区while (1){memset(temp, 0, sizeof(temp));int ret = recv(client, temp, sizeof(temp), 0); //接收数据if (SOCKET_ERROR == ret)continue;Status = SEND_YET; //设置转发状态为未转发if(client==ClientSock[0].sClient ) { //设置防止出现自己给自己发消息的BUGflag=1;}else{flag=0;} memcpy( ClientSock[!flag].buf, temp, sizeof( ClientSock[!flag].buf));_beginthreadex(NULL, 0, ThreadSend, &flag, 0, NULL); //开启一个转发线程,flag标记着要转发给哪个客户端}return 0;
}//接受请求线程函数的定义
unsigned __stdcall ThreadAccept(void* param)
{int i = 0;int temp1 = 0, temp2 = 0;{while (i < 2) //表明只允许两个客户端连接{if (ClientSock[i].flag != 0){++i;continue;}//如果有客户端申请连接就接受连接int len= sizeof(ClientAddr);int ret = ClientSock[i].sClient = accept(ServerSocket, (SOCKADDR*)&ClientAddr, &len);//accept()是在一个套接口接受的一个连接。if (INVALID_SOCKET == ret){printf("accept error!\n");closesocket(ServerSocket);WSACleanup();return -1;}recv(ClientSock[i].sClient, ClientSock[i].userName, sizeof(ClientSock[i].userName), 0); //接收用户名printf(" 成功连上聊天用户!IP:%s ,Port: %d,UerName: %s\n",inet_ntoa(ClientAddr.sin_addr), htons(ClientAddr.sin_port), ClientSock[i].userName);memcpy(ClientSock[i].IP, inet_ntoa(ClientAddr.sin_addr), sizeof(ClientSock[i].IP)); //记录客户端IPClientSock[i].flag = ClientSock[i].sClient; //不同的socke有不同UINT_PTR类型的数字来标识i++;}i = 0;if (ClientSock[0].flag != 0 && ClientSock[1].flag != 0 ) //当两个用户都连接上服务器后才进行消息转发{if (ClientSock[0].flag != temp1) //每次断开一个连接后再次连上会新开一个线程,导致cpu使用率上升,所以要关掉旧的,,int temp1 = 0, temp2 = 0;{if (g_hRecv1) //这里关闭了线程句柄CloseHandle(g_hRecv1);g_hRecv1 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &ClientSock[0].flag, 0, NULL); //开启2个接收消息的线程} if (ClientSock[1].flag != temp2){if (g_hRecv2)CloseHandle(g_hRecv2);g_hRecv2 = (HANDLE)_beginthreadex(NULL, 0, ThreadRecv, &ClientSock[1].flag, 0, NULL);} }temp1 = ClientSock[0].flag; //防止ThreadRecv线程多次开启temp2 = ClientSock[1].flag;Sleep(2000);}return 0;}
客户端源码:
#include "WinSock2.h" // winsock2.h是套接字接口。
#include "process.h" //process.h 是包含用于和宏指令的作用声明与螺纹和过程一起使用的C标头文件。
#include "stdio.h" //被包含的文件通常是由系统提供的,其扩展名为.h,而stdio为standard input output的缩写,意为“标准输入输出”
#include "stdlib.h" //stdlib 头文件里包含了C语言的一些函数,该文件包含了的C语言标准库函数的定义
#include "conio.h" //将conio.h包含入你的程序,使你可以引用其中声明的函数。conio.h不是C标准库中的头文件。
#pragma comment(lib,"ws2_32.lib") // ws2_32.lib是套接字实现。
#define RECV_OVER 1
#define RECV_YET 0
char userName[20] = { 0 };
int Status = RECV_YET;
unsigned __stdcall ThreadRecv(void* param); //接受数据的线程
unsigned __stdcall ThreadSend(void* param); //发送数据的线程
int main(void)
{WSADATA wsaData = { 0 }; SOCKET ClientSocket = INVALID_SOCKET; //客户端套接字sockaddr_in ServerAddr = { 0 }; //服务端地址,SOCKADDR_IN为结构体,可以小写unsigned short SERVERPORT = 6666;
//初始化套接字WSADATA data;int ret=WSAStartup(MAKEWORD(2,2),&data); //WSAStartup函数的返回值是0表示成功 if (SOCKET_ERROR==ret) {printf("WSAStartup 启动错误!\n"); return -1;}//创建套接字ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if ( INVALID_SOCKET == ClientSocket){printf("socket创建失败!\n");WSACleanup();return -1;}//输入服务器IPprintf("请输入服务器的IP:");char IP[32] = { 0 };gets_s(IP,31);//设置服务器地址ServerAddr.sin_family = AF_INET; ServerAddr.sin_port = htons(SERVERPORT); //服务器端口ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP); //服务器地址printf("正在努力连接服务器.....\n");//连接服务器ret=connect(ClientSocket,(sockaddr*)&ServerAddr,sizeof(ServerAddr));if ( SOCKET_ERROR ==ret) {printf("connect连接服务器失败!\n");closesocket(ClientSocket);WSACleanup();return -1;}printf("成功连上服务器 IP:%s Port:%d\n",IP,htons(ServerAddr.sin_port));printf("欢迎登录66微聊聊天室!\n");printf("请输入你的名字: ");gets_s(userName,20);send(ClientSocket, userName, sizeof(userName), 0);printf("\n\n");_beginthreadex(NULL, 0, ThreadRecv, &ClientSocket, 0, NULL); //启动接收和发送消息线程_beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL);for (int k = 0;k < 1000;k++) // 让主线程休眠,不让它关闭TCP连接{Sleep(10000000);}closesocket(ClientSocket);WSACleanup();return 0;
}unsigned __stdcall ThreadRecv(void* param)
{char buf[512] = { 0 };while (1){int ret = recv(*(SOCKET*)param, buf, sizeof(buf), 0); //先强行转化为SOCKET*,不然void*直接引用会出错if (SOCKET_ERROR == ret){Sleep(500); //将进程挂起一段时间,即停下来0.5s再继续continue;}if (strlen(buf) != 0) //判断缓冲区是不是有数据,有就打印出来!{printf("%s\n", buf);Status = RECV_OVER; //把状态设为已接收}elseSleep(100); }return 0;
}unsigned __stdcall ThreadSend(void* param)
{char buf[512] = { 0 };int ret = 0;while (1){int c = getch(); //在头文件conio.h有定义,此函数是一个不回显函数,当用户按下某个字符时,此函数自动读取,无需按回车if(c == 72 || c == 0 || c == 68) //遇到这几个值getch就会自动跳过continue; printf("%s: ", userName);gets_s(buf,511); //此函数在stdio.h中定义,第二参数就是允许输入长度,留一位补零,否则溢出ret = send(*(SOCKET*)param, buf, sizeof(buf), 0);if (ret == SOCKET_ERROR)if (ret == SOCKET_ERROR){return 1;}if(strcmp(buf,"bye") == 0||strcmp(buf,"再见")==0) {printf("您已退出聊天室!\n");break;}}return 0;
}
最后:
需要代码的可以自行下载。下载链接
下载操作: