socket多人聊天程序C语言版(一)地址: http://www.jb51.net/article/94938.htm
1V1实现了,1V多也就容易了。不过相对于1V1的程序,我经过大改,采用链表来动态管理。这样效率真的提升不少,至少CPU使用率稳稳的在20以下,不会飙到100了。用C语言写这个还是挺费时间的,因为什么功能函数都要自己写,不像C++有STL库可以用,MFC写就更简单了,接下来我还会更新MFC版本的多人聊天程序。好了,废话少说,进入主题。
这个程序要解决的问题如下:
1.CPU使用率飙升问题 –>用链表动态管理
2.用户自定义聊天,就是想跟谁聊跟谁聊 –> _Client结构体中新增一个ChatName字段,用来表示要和谁聊天,这个字段很重要,因为server转发消息的时候就是按照这个字段来转发的。
3.中途换人聊天,就是聊着聊着,想和别人聊,而且自己还一样能接收到其它人发的消息 –> 这个就要小改客户端的代码了,可以在发送聊天消息之前插入一段代码,用来切换聊天用户。具体做法就是,用getch()函数读取ESC键,如果用户按了这个键,则表示想切换用户,然后会输出一行提示,请输入chat name,就是想要和谁聊天的名字,发送这个名字过去之前要加一个标识符,表示这个消息是切换聊天用户消息。然后server接收到这个消息后会判断第一个字符是不是标识符,第二个字符不能是标识符,则根据这个name来查找当前在线的用户,然后修改想切换聊天用户的ChatName为name这个用户。(可能有点绕,不懂的看代码就清晰易懂了~)
4.下线后提醒对方 –> 还是老套路,只要send对方不通就当对方下线了。
编写环境:WIN10,VS2015
效果图:
为了方便就不用虚拟机演示了,但是在虚拟机是肯定可以的,应该说只要是局域网,能互相ping通就可以使用这个程序。
Server code:
链表头文件:
#ifndef _CLIENT_LINK_LIST_H_#define _CLIENT_LINK_LIST_H_#include <WinSock2.h>#include <stdio.h>//客户端信息结构体typedef struct _Client{
SOCKET sClient;
//客户端套接字 char buf[128];
//数据缓冲区 char userName[16];
//客户端用户名 char IP[20];
//客户端IP unsigned short Port;
//客户端端口 UINT_PTR flag;
//标记客户端,用来区分不同的客户端 char ChatName[16];
//指定要和哪个客户端聊天 _Client* next;
//指向下一个结点}
Client, *pClient;
/* * function 初始化链表 * return 无返回值 */void Init();
/* * function 获取头节点 * return 返回头节点 */pClient GetHeadNode();
/* * function 添加一个客户端 * param client表示一个客户端对象 * return 无返回值 */void AddClient(pClient client);
/* * function 删除一个客户端 * param flag标识一个客户端对象 * return 返回true表示删除成功,false表示失败 */bool RemoveClient(UINT_PTR flag);
/* * function 根据name查找指定客户端 * param name是指定客户端的用户名 * return 返回一个client表示查找成功,返回INVALID_SOCKET表示无此用户 */SOCKET FindClient(char* name);
/* * function 根据SOCKET查找指定客户端 * param client是指定客户端的套接字 * return 返回一个pClient表示查找成功,返回NULL表示无此用户 */pClient FindClient(SOCKET client);
/* * function 计算客户端连接数 * param client表示一个客户端对象 * return 返回连接数 */int CountCon();
/* * function 清空链表 * return 无返回值 */void ClearClient();
/* * function 检查连接状态并关闭一个连接 * return 返回值 */void CheckConnection();
/* * function 指定发送给哪个客户端 * param FromName,发信人 * param ToName, 收信人 * param data, 发送的消息 */void SendData(char* FromName, char* ToName, char* data);
#endif //_CLIENT_LINK_LIST_H_
链表cpp文件:
#include "ClientLinkList.h"pClient head = (pClient)malloc(sizeof(_Client));
//创建一个头结点/* * function 初始化链表 * return 无返回值 */void Init(){
head->next = NULL;
}
/* * function 获取头节点 * return 返回头节点 */pClient GetHeadNode(){
return head;
}
/* * function 添加一个客户端 * param client表示一个客户端对象 * return 无返回值 */void AddClient(pClient client){
client->next = head->next;
//比如:head->1->2,然后添加一个3进来后是 head->next = client;
//3->1->2,head->3->1->2}
/* * function 删除一个客户端 * param flag标识一个客户端对象 * return 返回true表示删除成功,false表示失败 */bool RemoveClient(UINT_PTR flag){
//从头遍历,一个个比较 pClient pCur = head->next;
//pCur指向第一个结点 pClient pPre = head;
//pPre指向head while (pCur) {
// head->1->2->3->4,要删除2,则直接让1->3 if (pCur->flag == flag) {
pPre->next = pCur->next;
closesocket(pCur->sClient);
//关闭套接字 free(pCur);
//释放该结点 return true;
}
pPre = pCur;
pCur = pCur->next;
}
return false;
}
/* * function 查找指定客户端 * param name是指定客户端的用户名 * return 返回socket表示查找成功,返回INVALID_SOCKET表示无此用户 */SOCKET FindClient(char* name){
//从头遍历,一个个比较 pClient pCur = head;
while (pCur = pCur->next) {
if (strcmp(pCur->userName, name) == 0) return pCur->sClient;
}
return INVALID_SOCKET;
}
/* * function 根据SOCKET查找指定客户端 * param client是指定客户端的套接字 * return 返回一个pClient表示查找成功,返回NULL表示无此用户 */pClient FindClient(SOCKET client){
//从头遍历,一个个比较 pClient pCur = head;
while (pCur = pCur->next) {
if (pCur->sClient == client) return pCur;
}
return NULL;
}
/* * function 计算客户端连接数 * param client表示一个客户端对象 * return 返回连接数 */int CountCon(){
int iCount = 0;
pClient pCur = head;
while (pCur = pCur->next) iCount++;
return iCount;
}
/* * function 清空链表 * return 无返回值 */void ClearClient(){
pClient pCur = head->next;
pClient pPre = head;
while (pCur) {
//head->1->2->3->4,先删除1,head->2,然后free 1 pClient p = pCur;
pPre->next = p->next;
free(p);
pCur = pPre->next;
}
}
/* * function 检查连接状态并关闭一个连接 * return 返回值 */void CheckConnection(){
pClient pclient = GetHeadNode();
while (pclient = pclient->next) {
if (send(pclient->sClient, "", sizeof(""), 0) == SOCKET_ERROR) {
if (pclient->sClient != 0) {
printf("Disconnect from IP: %s,UserName: %sn", pclient->IP, pclient->userName);
char error[128] = {
0 }
;
//发送下线消息给发消息的人 sprintf(error, "The %s was downline.n", pclient->userName);
send(FindClient(pclient->ChatName), error, sizeof(error), 0);
closesocket(pclient->sClient);
//这里简单的判断:若发送消息失败,则认为连接中断(其原因有多种),关闭该套接字 RemoveClient(pclient->flag);
break;
}
}
}
}
/* * function 指定发送给哪个客户端 * param FromName,发信人 * param ToName, 收信人 * param data, 发送的消息 */void SendData(char* FromName, char* ToName, char* data){
SOCKET client = FindClient(ToName);
//查找是否有此用户 char error[128] = {
0 }
;
int ret = 0;
if (client != INVALID_SOCKET && strlen(data) != 0) {
char buf[128] = {
0 }
;
sprintf(buf, "%s: %s", FromName, data);
//添加发送消息的用户名 ret = send(client, buf, sizeof(buf), 0);
}
else//发送错误消息给发消息的人 {
if(client == INVALID_SOCKET) sprintf(error, "The %s was downline.n", ToName);
else sprintf(error, "Send to %s message not allow empty, Please try again!n", ToName);
send(FindClient(FromName), error, sizeof(error), 0);
}
if (ret == SOCKET_ERROR)//发送下线消息给发消息的人 {
sprintf(error, "The %s was downline.n", ToName);
send(FindClient(FromName), error, sizeof(error), 0);
}
}
server cpp:
/*#include <WinSock2.h>#include <process.h>#include <stdlib.h>#include "ClientLinkList.h"#pragma comment(lib,"ws2_32.lib")SOCKET g_ServerSocket = INVALID_SOCKET;
//服务端套接字SOCKADDR_IN g_ClientAddr = {
0 }
;
//客户端地址int g_iClientAddrLen = sizeof(g_ClientAddr);
typedef struct _Send{
char FromName[16];
char ToName[16];
char data[128];
}
Send,*pSend;
//发送数据线程unsigned __stdcall ThreadSend(void* param){
pSend psend = (pSend)param;
//转换为Send类型 SendData(psend->FromName, psend->ToName, psend->data);
//发送数据 return 0;
}
//接受数据unsigned __stdcall ThreadRecv(void* param){
int ret = 0;
while (1) {
pClient pclient = (pClient)param;
if (!pclient) return 1;
ret = recv(pclient->sClient, pclient->buf, sizeof(pclient->buf), 0);
if (ret == SOCKET_ERROR) return 1;
if (pclient->buf[0] == '#' && pclient->buf[1] != '#') //#表示用户要指定另一个用户进行聊天 {
SOCKET socket = FindClient(&pclient->buf[1]);
//验证一下客户是否存在 if (socket != INVALID_SOCKET) {
pClient c = (pClient)malloc(sizeof(_Client));
c = FindClient(socket);
//只要改变ChatName,发送消息的时候就会自动发给指定的用户了 memset(pclient->ChatName, 0, sizeof(pclient->ChatName));
memcpy(pclient->ChatName , c->userName,sizeof(pclient->ChatName));
}
else send(pclient->sClient, "The user have not online or not exits.",64,0);
continue;
}
pSend psend = (pSend)malloc(sizeof(_Send));
//把发送人的用户名和接收消息的用户和消息赋值给结构体,然后当作参数传进发送消息进程中 memcpy(psend->FromName, pclient->userName, sizeof(psend->FromName));
memcpy(psend->ToName, pclient->ChatName, sizeof(psend->ToName));
memcpy(psend->data, pclient->buf, sizeof(psend->data));
_beginthreadex(NULL, 0, ThreadSend, psend, 0, NULL);
Sleep(200);
}
return 0;
}
//开启接收消息线程void StartRecv(){
pClient pclient = GetHeadNode();
while (pclient = pclient->next) _beginthreadex(NULL, 0, ThreadRecv, pclient, 0, NULL);
}
//管理连接unsigned __stdcall ThreadManager(void* param){
while (1) {
CheckConnection();
//检查连接状况 Sleep(2000);
//2s检查一次 }
return 0;
}
//接受请求unsigned __stdcall ThreadAccept(void* param){
_beginthreadex(NULL, 0, ThreadManager, NULL, 0, NULL);
Init();
//初始化一定不要再while里面做,否则head会一直为NULL!!! while (1) {
//创建一个新的客户端对象 pClient pclient = (pClient)malloc(sizeof(_Client));
//如果有客户端申请连接就接受连接 if ((pclient->sClient = accept(g_ServerSocket, (SOCKADDR*)&g_ClientAddr, &g_iClientAddrLen)) == INVALID_SOCKET) {
printf("accept failed with error code: %dn", WSAGetLastError());
closesocket(g_ServerSocket);
WSACleanup();
return -1;
}
recv(pclient->sClient, pclient->userName, sizeof(pclient->userName), 0);
//接收用户名和指定聊天对象的用户名 recv(pclient->sClient, pclient->ChatName, sizeof(pclient->ChatName), 0);
memcpy(pclient->IP, inet_ntoa(g_ClientAddr.sin_addr), sizeof(pclient->IP));
//记录客户端IP pclient->flag = pclient->sClient;
//不同的socke有不同UINT_PTR类型的数字来标识 pclient->Port = htons(g_ClientAddr.sin_port);
AddClient(pclient);
//把新的客户端加入链表中 printf("Successfuuly got a connection from IP:%s ,Port: %d,UerName: %s , ChatName: %sn", pclient->IP, pclient->Port, pclient->userName,pclient->ChatName);
if (CountCon() >= 2) //当至少两个用户都连接上服务器后才进行消息转发 StartRecv();
Sleep(2000);
}
return 0;
}
//启动服务器int StartServer(){
//存放套接字信息的结构 WSADATA wsaData = {
0 }
;
SOCKADDR_IN ServerAddr = {
0 }
;
//服务端地址 USHORT uPort = 18000;
//服务器监听端口 //初始化套接字 if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
printf("WSAStartup failed with error code: %dn", WSAGetLastError());
return -1;
}
//判断版本 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("wVersion was not 2.2n");
return -1;
}
//创建套接字 g_ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (g_ServerSocket == INVALID_SOCKET) {
printf("socket failed with error code: %dn", WSAGetLastError());
return -1;
}
//设置服务器地址 ServerAddr.sin_family = AF_INET;
//连接方式 ServerAddr.sin_port = htons(uPort);
//服务器监听端口 ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//任何客户端都能连接这个服务器 //绑定服务器 if (SOCKET_ERROR == bind(g_ServerSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr))) {
printf("bind failed with error code: %dn", WSAGetLastError());
closesocket(g_ServerSocket);
return -1;
}
//设置监听客户端连接数 if (SOCKET_ERROR == listen(g_ServerSocket, 20000)) {
printf("listen failed with error code: %dn", WSAGetLastError());
closesocket(g_ServerSocket);
WSACleanup();
return -1;
}
_beginthreadex(NULL, 0, ThreadAccept, NULL, 0, 0);
for (int k = 0;
k < 100;
k++) //让主线程休眠,不让它关闭TCP连接. Sleep(10000000);
//关闭套接字 ClearClient();
closesocket(g_ServerSocket);
WSACleanup();
return 0;
}
int main(){
StartServer();
//启动服务器 return 0;
}
Client code:
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <WinSock2.h>#include <process.h>#include <stdio.h>#include <stdlib.h>#include <conio.h>#pragma comment(lib,"ws2_32.lib")#define RECV_OVER 1#define RECV_YET 0char userName[16] = {
0 }
;
char chatName[16] = {
0 }
;
int iStatus = RECV_YET;
//接受数据unsigned __stdcall ThreadRecv(void* param){
char buf[128] = {
0 }
;
while (1) {
int ret = recv(*(SOCKET*)param, buf, sizeof(buf), 0);
if (ret == SOCKET_ERROR) {
Sleep(500);
continue;
}
if (strlen(buf) != 0) {
printf("%sn", buf);
iStatus = RECV_OVER;
}
else Sleep(100);
}
return 0;
}
//发送数据unsigned __stdcall ThreadSend(void* param){
char buf[128] = {
0 }
;
int ret = 0;
while (1) {
int c = getch();
if (c == 27) //ESC ASCII是27 {
memset(buf, 0, sizeof(buf));
printf("Please input the chat name:");
gets_s(buf);
char b[17] = {
0 }
;
sprintf(b, "#%s", buf);
ret = send(*(SOCKET*)param,b , sizeof(b), 0);
if (ret == SOCKET_ERROR) return 1;
continue;
}
if(c == 72 || c == 0 || c == 68)//为了显示美观,加一个无回显的读取字符函数 continue;
//getch返回值我是经过实验得出如果是返回这几个值,则getch就会自动跳过,具体我也不懂。 printf("%s: ", userName);
gets_s(buf);
ret = send(*(SOCKET*)param, buf, sizeof(buf), 0);
if (ret == SOCKET_ERROR) return 1;
}
return 0;
}
//连接服务器int ConnectServer(){
WSADATA wsaData = {
0 }
;
//存放套接字信息 SOCKET ClientSocket = INVALID_SOCKET;
//客户端套接字 SOCKADDR_IN ServerAddr = {
0 }
;
//服务端地址 USHORT uPort = 18000;
//服务端端口 //初始化套接字 if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {
printf("WSAStartup failed with error code: %dn", WSAGetLastError());
return -1;
}
//判断套接字版本 if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("wVersion was not 2.2n");
return -1;
}
//创建套接字 ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ClientSocket == INVALID_SOCKET) {
printf("socket failed with error code: %dn", WSAGetLastError());
return -1;
}
//输入服务器IP printf("Please input server IP:");
char IP[32] = {
0 }
;
gets_s(IP);
//设置服务器地址 ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(uPort);
//服务器端口 ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP);
//服务器地址 printf("connecting......n");
//连接服务器 if (SOCKET_ERROR == connect(ClientSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr))) {
printf("connect failed with error code: %dn", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return -1;
}
printf("Connecting server successfully IP:%s Port:%dn", IP, htons(ServerAddr.sin_port));
printf("Please input your UserName: ");
gets_s(userName);
send(ClientSocket, userName, sizeof(userName), 0);
printf("Please input the ChatName: ");
gets_s(chatName);
send(ClientSocket, chatName, sizeof(chatName), 0);
printf("nn");
_beginthreadex(NULL, 0, ThreadRecv, &ClientSocket, 0, NULL);
//启动接收和发送消息线程 _beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL);
for (int k = 0;
k < 1000;
k++) Sleep(10000000);
closesocket(ClientSocket);
WSACleanup();
return 0;
}
int main(){
ConnectServer();
//连接服务器 return 0;
}
最后,需要改进的有以下几点:
1.没有消息记录,所以最好用文件或者数据库的方式记录,个人推荐数据库。
2.没有用户注册,登陆的操作,也是用文件或者数据库来弄。程序一运行就读取数据库信息就行。
3.群聊功能没有弄,这个其实很简单,就是服务器不管3721,把接收到的消息转发给所有在线用户。
4.没有离线消息,这个就用数据库存储离线消息,然后用户上线后立即发送过去就行。
最后总结一下,没有数据库的聊天程序果然功能简陋~,C语言写的程序要注意对内存的操作。还有TCP方式的连接太费时费内存(用户量达的时候)。
C语言版聊天程序(TCP版本,接下来还有UDP版本)到这里结束~,欢迎各位提出自己的看法。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。