程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> socket多人聊天程序C語言版(二)

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

編輯:關於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方式的連接太費時費內存(用戶量達的時候)。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved