socket多人聊天程序C语言版(二)

来源:互联网 发布:淘宝客服怎么找 编辑:程序博客网 时间:2024/05/17 07:03

socket多人聊天程序C语言版(一)地址:
http://blog.csdn.net/qq_18297675/article/details/52819975

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: %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 <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: %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 <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("%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方式的连接太费时费内存(用户量达的时候)。

C语言版聊天程序(TCP版本,接下来还有UDP版本)到这里结束~,欢迎各位提出自己的看法。

1 0