socket多人聊天程序C語言版(二)
socket多人聊天程序C語言版(一)地址:
1V1實(shí)現(xiàn)了,1V多也就容易了。不過相對(duì)于1V1的程序,我經(jīng)過大改,采用鏈表來動(dòng)態(tài)管理。這樣效率真的提升不少,至少CPU使用率穩(wěn)穩(wěn)的在20以下,不會(huì)飆到100了。用C語言寫這個(gè)還是挺費(fèi)時(shí)間的,因?yàn)槭裁垂δ芎瘮?shù)都要自己寫,不像C++有STL庫可以用,MFC寫就更簡(jiǎn)單了,接下來我還會(huì)更新MFC版本的多人聊天程序。好了,廢話少說,進(jìn)入主題。
這個(gè)程序要解決的問題如下:
1.CPU使用率飆升問題 –>用鏈表動(dòng)態(tài)管理
2.用戶自定義聊天,就是想跟誰聊跟誰聊 –> _Client結(jié)構(gòu)體中新增一個(gè)ChatName字段,用來表示要和誰聊天,這個(gè)字段很重要,因?yàn)閟erver轉(zhuǎn)發(fā)消息的時(shí)候就是按照這個(gè)字段來轉(zhuǎn)發(fā)的。
3.中途換人聊天,就是聊著聊著,想和別人聊,而且自己還一樣能接收到其它人發(fā)的消息 –> 這個(gè)就要小改客戶端的代碼了,可以在發(fā)送聊天消息之前插入一段代碼,用來切換聊天用戶。具體做法就是,用getch()函數(shù)讀取ESC鍵,如果用戶按了這個(gè)鍵,則表示想切換用戶,然后會(huì)輸出一行提示,請(qǐng)輸入chat name,就是想要和誰聊天的名字,發(fā)送這個(gè)名字過去之前要加一個(gè)標(biāo)識(shí)符,表示這個(gè)消息是切換聊天用戶消息。然后server接收到這個(gè)消息后會(huì)判斷第一個(gè)字符是不是標(biāo)識(shí)符,第二個(gè)字符不能是標(biāo)識(shí)符,則根據(jù)這個(gè)name來查找當(dāng)前在線的用戶,然后修改想切換聊天用戶的ChatName為name這個(gè)用戶。(可能有點(diǎn)繞,不懂的看代碼就清晰易懂了~)
4.下線后提醒對(duì)方 –> 還是老套路,只要send對(duì)方不通就當(dāng)對(duì)方下線了。
編寫環(huán)境:WIN10,VS2015
效果圖:
為了方便就不用虛擬機(jī)演示了,但是在虛擬機(jī)是肯定可以的,應(yīng)該說只要是局域網(wǎng),能互相ping通就可以使用這個(gè)程序。



Server code:
鏈表頭文件:
#ifndef _CLIENT_LINK_LIST_H_
#define _CLIENT_LINK_LIST_H_
#include <WinSock2.h>
#include <stdio.h>
//客戶端信息結(jié)構(gòu)體
typedef struct _Client
{
SOCKET sClient; //客戶端套接字
char buf[128]; //數(shù)據(jù)緩沖區(qū)
char userName[16]; //客戶端用戶名
char IP[20]; //客戶端IP
unsigned short Port; //客戶端端口
UINT_PTR flag; //標(biāo)記客戶端,用來區(qū)分不同的客戶端
char ChatName[16]; //指定要和哪個(gè)客戶端聊天
_Client* next; //指向下一個(gè)結(jié)點(diǎn)
}Client, *pClient;
/* * function 初始化鏈表 * return 無返回值 */
void Init();
/* * function 獲取頭節(jié)點(diǎn) * return 返回頭節(jié)點(diǎn) */
pClient GetHeadNode();
/* * function 添加一個(gè)客戶端 * param client表示一個(gè)客戶端對(duì)象 * return 無返回值 */
void AddClient(pClient client);
/* * function 刪除一個(gè)客戶端 * param flag標(biāo)識(shí)一個(gè)客戶端對(duì)象 * return 返回true表示刪除成功,false表示失敗 */
bool RemoveClient(UINT_PTR flag);
/* * function 根據(jù)name查找指定客戶端 * param name是指定客戶端的用戶名 * return 返回一個(gè)client表示查找成功,返回INVALID_SOCKET表示無此用戶 */
SOCKET FindClient(char* name);
/* * function 根據(jù)SOCKET查找指定客戶端 * param client是指定客戶端的套接字 * return 返回一個(gè)pClient表示查找成功,返回NULL表示無此用戶 */
pClient FindClient(SOCKET client);
/* * function 計(jì)算客戶端連接數(shù) * param client表示一個(gè)客戶端對(duì)象 * return 返回連接數(shù) */
int CountCon();
/* * function 清空鏈表 * return 無返回值 */
void ClearClient();
/* * function 檢查連接狀態(tài)并關(guān)閉一個(gè)連接 * return 返回值 */
void CheckConnection();
/* * function 指定發(fā)送給哪個(gè)客戶端 * param FromName,發(fā)信人 * param ToName, 收信人 * param data, 發(fā)送的消息 */
void SendData(char* FromName, char* ToName, char* data);
#endif //_CLIENT_LINK_LIST_H_
鏈表cpp文件:
#include "ClientLinkList.h"
pClient head = (pClient)malloc(sizeof(_Client)); //創(chuàng)建一個(gè)頭結(jié)點(diǎn)
/* * function 初始化鏈表 * return 無返回值 */
void Init()
{
head->next = NULL;
}
/* * function 獲取頭節(jié)點(diǎn) * return 返回頭節(jié)點(diǎn) */
pClient GetHeadNode()
{
return head;
}
/* * function 添加一個(gè)客戶端 * param client表示一個(gè)客戶端對(duì)象 * return 無返回值 */
void AddClient(pClient client)
{
client->next = head->next; //比如:head->1->2,然后添加一個(gè)3進(jìn)來后是
head->next = client; //3->1->2,head->3->1->2
}
/* * function 刪除一個(gè)客戶端 * param flag標(biāo)識(shí)一個(gè)客戶端對(duì)象 * return 返回true表示刪除成功,false表示失敗 */
bool RemoveClient(UINT_PTR flag)
{
//從頭遍歷,一個(gè)個(gè)比較
pClient pCur = head->next;//pCur指向第一個(gè)結(jié)點(diǎn)
pClient pPre = head; //pPre指向head
while (pCur)
{
// head->1->2->3->4,要?jiǎng)h除2,則直接讓1->3
if (pCur->flag == flag)
{
pPre->next = pCur->next;
closesocket(pCur->sClient); //關(guān)閉套接字
free(pCur); //釋放該結(jié)點(diǎn)
return true;
}
pPre = pCur;
pCur = pCur->next;
}
return false;
}
/* * function 查找指定客戶端 * param name是指定客戶端的用戶名 * return 返回socket表示查找成功,返回INVALID_SOCKET表示無此用戶 */
SOCKET FindClient(char* name)
{
//從頭遍歷,一個(gè)個(gè)比較
pClient pCur = head;
while (pCur = pCur->next)
{
if (strcmp(pCur->userName, name) == 0)
return pCur->sClient;
}
return INVALID_SOCKET;
}
/* * function 根據(jù)SOCKET查找指定客戶端 * param client是指定客戶端的套接字 * return 返回一個(gè)pClient表示查找成功,返回NULL表示無此用戶 */
pClient FindClient(SOCKET client)
{
//從頭遍歷,一個(gè)個(gè)比較
pClient pCur = head;
while (pCur = pCur->next)
{
if (pCur->sClient == client)
return pCur;
}
return NULL;
}
/* * function 計(jì)算客戶端連接數(shù) * param client表示一個(gè)客戶端對(duì)象 * return 返回連接數(shù) */
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 檢查連接狀態(tài)并關(guān)閉一個(gè)連接 * 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 }; //發(fā)送下線消息給發(fā)消息的人
sprintf(error, "The %s was downline.\n", pclient->userName);
send(FindClient(pclient->ChatName), error, sizeof(error), 0);
closesocket(pclient->sClient); //這里簡(jiǎn)單的判斷:若發(fā)送消息失敗,則認(rèn)為連接中斷(其原因有多種),關(guān)閉該套接字
RemoveClient(pclient->flag);
break;
}
}
}
}
/* * function 指定發(fā)送給哪個(gè)客戶端 * param FromName,發(fā)信人 * param ToName, 收信人 * param data, 發(fā)送的消息 */
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); //添加發(fā)送消息的用戶名
ret = send(client, buf, sizeof(buf), 0);
}
else//發(fā)送錯(cuò)誤消息給發(fā)消息的人
{
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)//發(fā)送下線消息給發(fā)消息的人
{
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; //服務(wù)端套接字
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;
//發(fā)送數(shù)據(jù)線程
unsigned __stdcall ThreadSend(void* param)
{
pSend psend = (pSend)param; //轉(zhuǎn)換為Send類型
SendData(psend->FromName, psend->ToName, psend->data); //發(fā)送數(shù)據(jù)
return 0;
}
//接受數(shù)據(jù)
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] != '#') //#表示用戶要指定另一個(gè)用戶進(jìn)行聊天
{
SOCKET socket = FindClient(&pclient->buf[1]); //驗(yàn)證一下客戶是否存在
if (socket != INVALID_SOCKET)
{
pClient c = (pClient)malloc(sizeof(_Client));
c = FindClient(socket); //只要改變ChatName,發(fā)送消息的時(shí)候就會(huì)自動(dòng)發(fā)給指定的用戶了
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));
//把發(fā)送人的用戶名和接收消息的用戶和消息賦值給結(jié)構(gòu)體,然后當(dāng)作參數(shù)傳進(jìn)發(fā)送消息進(jìn)程中
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;
}
//接受請(qǐng)求
unsigned __stdcall ThreadAccept(void* param)
{
_beginthreadex(NULL, 0, ThreadManager, NULL, 0, NULL);
Init(); //初始化一定不要再while里面做,否則head會(huì)一直為NULL?。?!
while (1)
{
//創(chuàng)建一個(gè)新的客戶端對(duì)象
pClient pclient = (pClient)malloc(sizeof(_Client));
//如果有客戶端申請(qǐng)連接就接受連接
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); //接收用戶名和指定聊天對(duì)象的用戶名
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類型的數(shù)字來標(biāo)識(shí)
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) //當(dāng)至少兩個(gè)用戶都連接上服務(wù)器后才進(jìn)行消息轉(zhuǎn)發(fā)
StartRecv();
Sleep(2000);
}
return 0;
}
//啟動(dòng)服務(wù)器
int StartServer()
{
//存放套接字信息的結(jié)構(gòu)
WSADATA wsaData = { 0 };
SOCKADDR_IN ServerAddr = { 0 }; //服務(wù)端地址
USHORT uPort = 18000; //服務(wù)器監(jiān)聽端口
//初始化套接字
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;
}
//創(chuàng)建套接字
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;
}
//設(shè)置服務(wù)器地址
ServerAddr.sin_family = AF_INET;//連接方式
ServerAddr.sin_port = htons(uPort);//服務(wù)器監(jiān)聽端口
ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//任何客戶端都能連接這個(gè)服務(wù)器
//綁定服務(wù)器
if (SOCKET_ERROR == bind(g_ServerSocket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr)))
{
printf("bind failed with error code: %d\n", WSAGetLastError());
closesocket(g_ServerSocket);
return -1;
}
//設(shè)置監(jiān)聽客戶端連接數(shù)
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++) //讓主線程休眠,不讓它關(guān)閉TCP連接.
Sleep(10000000);
//關(guān)閉套接字
ClearClient();
closesocket(g_ServerSocket);
WSACleanup();
return 0;
}
int main()
{
StartServer(); //啟動(dòng)服務(wù)器
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 0
char userName[16] = { 0 };
char chatName[16] = { 0 };
int iStatus = RECV_YET;
//接受數(shù)據(jù)
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;
}
//發(fā)送數(shù)據(jù)
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)//為了顯示美觀,加一個(gè)無回顯的讀取字符函數(shù)
continue; //getch返回值我是經(jīng)過實(shí)驗(yàn)得出如果是返回這幾個(gè)值,則getch就會(huì)自動(dòng)跳過,具體我也不懂。
printf("%s: ", userName);
gets_s(buf);
ret = send(*(SOCKET*)param, buf, sizeof(buf), 0);
if (ret == SOCKET_ERROR)
return 1;
}
return 0;
}
//連接服務(wù)器
int ConnectServer()
{
WSADATA wsaData = { 0 };//存放套接字信息
SOCKET ClientSocket = INVALID_SOCKET;//客戶端套接字
SOCKADDR_IN ServerAddr = { 0 };//服務(wù)端地址
USHORT uPort = 18000;//服務(wù)端端口
//初始化套接字
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;
}
//創(chuàng)建套接字
ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ClientSocket == INVALID_SOCKET)
{
printf("socket failed with error code: %d\n", WSAGetLastError());
return -1;
}
//輸入服務(wù)器IP
printf("Please input server IP:");
char IP[32] = { 0 };
gets_s(IP);
//設(shè)置服務(wù)器地址
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_port = htons(uPort);//服務(wù)器端口
ServerAddr.sin_addr.S_un.S_addr = inet_addr(IP);//服務(wù)器地址
printf("connecting......\n");
//連接服務(wù)器
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); //啟動(dòng)接收和發(fā)送消息線程
_beginthreadex(NULL, 0, ThreadSend, &ClientSocket, 0, NULL);
for (int k = 0;k < 1000;k++)
Sleep(10000000);
closesocket(ClientSocket);
WSACleanup();
return 0;
}
int main()
{
ConnectServer(); //連接服務(wù)器
return 0;
}
最后,需要改進(jìn)的有以下幾點(diǎn):
1.沒有消息記錄,所以最好用文件或者數(shù)據(jù)庫的方式記錄,個(gè)人推薦數(shù)據(jù)庫。
2.沒有用戶注冊(cè),登陸的操作,也是用文件或者數(shù)據(jù)庫來弄。程序一運(yùn)行就讀取數(shù)據(jù)庫信息就行。
3.群聊功能沒有弄,這個(gè)其實(shí)很簡(jiǎn)單,就是服務(wù)器不管3721,把接收到的消息轉(zhuǎn)發(fā)給所有在線用戶。
4.沒有離線消息,這個(gè)就用數(shù)據(jù)庫存儲(chǔ)離線消息,然后用戶上線后立即發(fā)送過去就行。
最后總結(jié)一下,沒有數(shù)據(jù)庫的聊天程序果然功能簡(jiǎn)陋~,C語言寫的程序要注意對(duì)內(nèi)存的操作。還有TCP方式的連接太費(fèi)時(shí)費(fèi)內(nèi)存(用戶量達(dá)的時(shí)候)。
C語言版聊天程序(TCP版本,接下來還有UDP版本)到這里結(jié)束,歡迎各位提出自己的看法。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(20.驗(yàn)證括號(hào))
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(20.驗(yàn)證括號(hào)),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C++逆向分析移除鏈表元素實(shí)現(xiàn)方法詳解
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(203.移除鏈表元素),本篇文章通過逆向分析的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2022-11-11
C++類與對(duì)象及構(gòu)造函數(shù)析構(gòu)函數(shù)基礎(chǔ)詳解
這篇文章主要為大家介紹了C++類與對(duì)象及構(gòu)造函數(shù)析構(gòu)函數(shù)基礎(chǔ)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
簡(jiǎn)單掌握Linux系統(tǒng)中fork()函數(shù)創(chuàng)建子進(jìn)程的用法
fork()函數(shù)只能在類Unix系統(tǒng)下使用,因?yàn)樾枰雞nistd頭文件,這里我們就來簡(jiǎn)單掌握Linux系統(tǒng)中fork()函數(shù)創(chuàng)建子進(jìn)程的用法,需要的朋友可以參考下2016-06-06

