socket多人聊天程序C语言版(二)
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 #include //客户端信息结构体 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: %s\n", 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 #include#include #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: %d\n", 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: %s\n", 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: %d\n", wsagetlasterror()); return -1; } //判断版本 if (lobyte(wsadata.wversion) != 2 || hibyte(wsadata.wversion) != 2) { printf("wversion was not 2.2\n"); return -1; } //创建套接字 g_serversocket = socket(af_inet, sock_stream, ipproto_tcp); if (g_serversocket == invalid_socket) { printf("socket failed with error code: %d\n", 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: %d\n", wsagetlasterror()); closesocket(g_serversocket); return -1; } //设置监听客户端连接数 if (socket_error == listen(g_serversocket, 20000)) { printf("listen failed with error code: %d\n", 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 #include#include #include #include #pragma comment(lib,"ws2_32.lib") #define recv_over 1 #define recv_yet 0 char 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("%s\n", 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: %d\n", wsagetlasterror()); return -1; } //判断套接字版本 if (lobyte(wsadata.wversion) != 2 || hibyte(wsadata.wversion) != 2) { printf("wversion was not 2.2\n"); return -1; } //创建套接字 clientsocket = socket(af_inet, sock_stream, ipproto_tcp); if (clientsocket == invalid_socket) { printf("socket failed with error code: %d\n", 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: %d\n", wsagetlasterror()); closesocket(clientsocket); wsacleanup(); return -1; } printf("connecting server successfully ip:%s port:%d\n", 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("\n\n"); _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方式的连接太费时费内存(用户量达的时候)。