來源:微信公眾號「編程學習基地」
C語言聊天室
基於 tcp 實現群聊功能,本項目設計是在windows環境下基於套接字(Socket)和多線程編程進行開發的簡易聊天室,實現了群聊功能,在VC6.0和VS2019運行測試無誤。
運行效果
分析設計
Windows下基於windows網絡接口Winsock的通信步驟為WSAStartup 進行初始化--> socket 創建套接字--> bind 綁定--> listen 監聽--> connect 連接--> accept 接收請求--> send/recv 發送或接收數據--> closesocket 關閉 socket--> WSACleanup 最終關閉。
了解完了一個 socket 的基本步驟后我們了解一下多線程以及線程的同步。
多線程
線程是進程的一條執行路徑,它包含獨立的堆棧和CPU寄存器狀態,每個線程共享所有的進程資源,包括打開的文件、信號標識及動態分配的內存等。一個進程內的所有線程使用同一個地址空間,而這些線程的執行由系統調度程序控制,調度程序決定哪個線程可執行以及什么時候執行線程。
簡而言之多線程是為了提高系統的運行效率。
Win32 API下的多線程編程 也就是兩個函數的應用CreateThread
以及WaitForSingleObject
,具體案例這里不多做介紹。
線程的同步
每個線程都可以訪問進程中的公共變量,資源,所以使用多線程的過程中需要注意的問題是如何防止兩個或兩個以上的線程同時訪問同一個數據,以免破壞數據的完整性。數據之間的相互制約包括
1、直接制約關系,即一個線程的處理結果,為另一個線程的輸入,因此線程之間直接制約着,這種關系可以稱之為同步關系
2、間接制約關系,即兩個線程需要訪問同一資源,該資源在同一時刻只能被一個線程訪問,這種關系稱之為線程間對資源的互斥訪問,某種意義上說互斥是一種制約關系更小的同步
windows線程間的同步方式有四種:臨界區、互斥量、信號量、事件。
本項目是基於事件內核對象實現的線程同步,事件內核對象是一種抽象的對象,有受信和未授信兩種狀態,通過等待WaitForSingleObject
實現線程同步。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //安全屬性
BOOL bManualReset, //是否手動重置事件對象為未受信對象
BOOL bInitialState, //指定事件對象創建時的初始狀態
LPCSTR lpName //事件對象的名稱
);
設置內核對象狀態
BOOL SetEvent(
HANDLE hEvent /*設置事件內核對象受信*/
);
BOOL ResetEvent(
HANDLE hEvent /*設置事件內核對象未受信*/
);
堵塞等待事件內核對象直到事件內核對象的狀態為受信。
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
具體使用閱讀全文在我的個人網站里看,篇幅太多。
服務端設計
在創建套接字綁定監聽之后會有一個等待連接的過程,在接收到新連接之后,需要創建一個線程來處理新連接,當有多個新連接時可通過創建多個線程來處理新連接,
定義最大連接數量以及最大套接字和最大線程
#define MAX_CLNT 256
int clnt_cnt = 0; //統計套接字
int clnt_socks[MAX_CLNT]; //管理套接字
HANDLE hThread[MAX_CLNT]; //管理線程
當有新連接來臨的時候創建線程處理新連接,並將新連接添加到套接字數組里面管理
hThread[clnt_cnt] = CreateThread(
NULL, // 默認安全屬性
NULL, // 默認堆棧大小
ThreadProc, // 線程入口地址(執行線程的函數)
(void*)&clnt_sock, // 傳給函數的參數
0, // 指定線程立即運行
&dwThreadId); // 返回線程的ID號
clnt_socks[clnt_cnt++] = clnt_sock;
線程的處理函數ThreadProc不做講解,大致就是數據的收以及群發。
主要講解線程同步,當有多個新連接來臨的時候,可能會造成多個線程同時訪問同一個數據(例如clnt_cnt)。這個時候就需要線程的同步來避免破壞數據的完整性。
首先是創建一個內核事件
HANDLE g_hEvent; /*事件內核對象*/
// 創建一個自動重置的(auto-reset events),受信的(signaled)事件內核對象
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
然后再需要訪問公共變量(例如clnt_cnt
)之前進行加鎖(設置等待),訪問完成之后解鎖(設置受信)
/*等待內核事件對象狀態受信*/
WaitForSingleObject(g_hEvent, INFINITE);
hThread[clnt_cnt] = CreateThread(NULL,NULL,ThreadProc,(void*)&clnt_sock,0,&dwThreadId);
clnt_socks[clnt_cnt++] = clnt_sock;
SetEvent(g_hEvent); /*設置受信*/
通過套接字數組來進行數據的轉發實現群聊功能,此時也用到了線程同步
void send_msg(char* msg, int len)
{
int i;
/*等待內核事件對象狀態受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
send(clnt_socks[i], msg, len, 0);
SetEvent(g_hEvent); /*設置受信*/
}
遇到的問題
等待線程返回的過程中最先用的是WaitForSingleObject
,很遺憾這是個阻塞函數,直到線程執行完成返回之后才會繼續往下執行,所以后面通過WaitForMultipleObjects
這個windowsAPI調用對hThread線程數組進行線程等待釋放。
整個過程不算太難,主要是僅僅實現了群聊功能,所以只需要了解windows下的網絡編程以及多線程編程和線程的同步方法就可以實現這個樣一個功能。
源代碼:
server.c
#include <winsock2.h> // 為了使用Winsock API函數
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define MAX_CLNT 256
#define BUF_SIZE 100
// 告訴連接器與WS2_32庫連接
#pragma comment(lib,"WS2_32.lib")
void error_handling(const char* msg); /*錯誤處理函數*/
DWORD WINAPI ThreadProc(LPVOID lpParam); /*線程執行函數*/
void send_msg(char* msg, int len); /*消息發送函數*/
HANDLE g_hEvent; /*事件內核對象*/
int clnt_cnt = 0; //統計套接字
int clnt_socks[MAX_CLNT]; //管理套接字
HANDLE hThread[MAX_CLNT]; //管理線程
int main()
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData); //請求了一個2.2版本的socket
// 創建一個自動重置的(auto-reset events),受信的(signaled)事件內核對象
g_hEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
// 創建套節字
SOCKET serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (serv_sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填充sockaddr_in結構
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888); //8888端口
sin.sin_addr.S_un.S_addr = INADDR_ANY; //本地地址
//sin.sin_addr.S_un.S_addr = inet_addr("169.254.211.52");
// 綁定這個套節字到一個本地地址
if (bind(serv_sock, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
error_handling("Failed bind()");
// 進入監聽模式
if (listen(serv_sock, 256) == SOCKET_ERROR) //最大連接數為2
error_handling("Failed listen()");
printf("Start listen:\n");
// 循環接受客戶的連接請求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
DWORD dwThreadId; /*線程ID*/
SOCKET clnt_sock;
//char szText[] = "hello!\n";
while (TRUE)
{
printf("等待新連接\n");
// 接受一個新連接
clnt_sock = accept(serv_sock, (SOCKADDR*)&remoteAddr, &nAddrLen);
if (clnt_sock == INVALID_SOCKET)
{
printf("Failed accept()");
continue;
}
/*等待內核事件對象狀態受信*/
WaitForSingleObject(g_hEvent, INFINITE);
hThread[clnt_cnt] = CreateThread(
NULL, // 默認安全屬性
NULL, // 默認堆棧大小
ThreadProc, // 線程入口地址(執行線程的函數)
(void*)&clnt_sock, // 傳給函數的參數
0, // 指定線程立即運行
&dwThreadId); // 返回線程的ID號
clnt_socks[clnt_cnt++] = clnt_sock;
SetEvent(g_hEvent); /*設置受信*/
printf(" 接受到一個連接:%s 執行線程ID:%d\r\n", inet_ntoa(remoteAddr.sin_addr), dwThreadId);
}
WaitForMultipleObjects(clnt_cnt, hThread, true, INFINITE);
for (int i = 0; i < clnt_cnt; i++)
{
CloseHandle(hThread[i]);
}
// 關閉監聽套節字
closesocket(serv_sock);
// 釋放WS2_32庫
WSACleanup();
return 0;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int clnt_sock = *((int*)lpParam);
int str_len = 0, i;
char msg[BUF_SIZE];
while ((str_len = recv(clnt_sock, msg, sizeof(msg), 0)) != -1)
{
send_msg(msg, str_len);
printf("群發送成功\n");
}
printf("客戶端退出:%d\n", GetCurrentThreadId());
/*等待內核事件對象狀態受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
{
if (clnt_sock == clnt_socks[i])
{
while (i++ < clnt_cnt - 1)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_cnt--;
SetEvent(g_hEvent); /*設置受信*/
// 關閉同客戶端的連接
closesocket(clnt_sock);
return NULL;
}
void send_msg(char* msg, int len)
{
int i;
/*等待內核事件對象狀態受信*/
WaitForSingleObject(g_hEvent, INFINITE);
for (i = 0; i < clnt_cnt; i++)
send(clnt_socks[i], msg, len, 0);
SetEvent(g_hEvent); /*設置受信*/
}
client.c
#include <winsock2.h>
#include <stdio.h>
#include <windows.h>
// 告訴連接器與WS2_32庫連接
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 256
#define NAME_SIZE 30
DWORD WINAPI send_msg(LPVOID lpParam);
DWORD WINAPI recv_msg(LPVOID lpParam);
void error_handling(const char* msg);
char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];
int main()
{
HANDLE hThread[2];
DWORD dwThreadId;
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
WSAStartup(sockVersion, &wsaData);
/*設置登錄用戶名*/
printf("Input your Chat Name:");
scanf("%s", name);
getchar(); //接收換行符
// 創建套節字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
error_handling("Failed socket()");
// 填寫遠程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(8888);
// 如果你的計算機沒有聯網,直接使用本地地址127.0.0.1
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (connect(sock, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
error_handling("Failed connect()");
printf("connect success\n");
hThread[0] = CreateThread(
NULL, // 默認安全屬性
NULL, // 默認堆棧大小
send_msg, // 線程入口地址(執行線程的函數)
&sock, // 傳給函數的參數
0, // 指定線程立即運行
&dwThreadId); // 返回線程的ID號
hThread[1] = CreateThread(
NULL, // 默認安全屬性
NULL, // 默認堆棧大小
recv_msg, // 線程入口地址(執行線程的函數)
&sock, // 傳給函數的參數
0, // 指定線程立即運行
&dwThreadId); // 返回線程的ID號
// 等待線程運行結束
WaitForMultipleObjects(2, hThread, true, INFINITE);
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
printf(" Thread Over,Enter anything to over.\n");
getchar();
// 關閉套節字
closesocket(sock);
// 釋放WS2_32庫
WSACleanup();
return 0;
}
DWORD WINAPI send_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
while (1)
{
fgets(msg, BUF_SIZE, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n"))
{
closesocket(sock);
exit(0);
}
sprintf(name_msg, "[%s]: %s", name, msg);
int nRecv = send(sock, name_msg, strlen(name_msg), 0);
}
return NULL;
}
DWORD WINAPI recv_msg(LPVOID lpParam)
{
int sock = *((int*)lpParam);
char name_msg[NAME_SIZE + BUF_SIZE];
int str_len;
while (1)
{
str_len = recv(sock, name_msg, NAME_SIZE + BUF_SIZE - 1, 0);
if (str_len == -1)
return -1;
name_msg[str_len] = 0;
fputs(name_msg, stdout);
}
return NULL;
}
void error_handling(const char* msg)
{
printf("%s\n", msg);
WSACleanup();
exit(1);
}