Windows C語言 Socket程式設計 server端(伺服器)--初級(多客戶端——初級版)
看過我的簡單版的伺服器程式碼的,會發現那段程式碼同一時間只能和一個客戶端通訊。這樣的程式碼能力很小侷限性很大。今天我來介紹一種多客戶端的伺服器程式碼。當然這段程式碼還是有問題的,至於是什麼問題我會在程式碼後面說清楚。
我的這個多客戶端的程式碼核心思想是多執行緒。在基本的伺服器的程式碼中簡單加一些內容就可以了。在accept的後面,我們每接到一個客戶端的連線請求,就會為這個客戶端建立一個單獨的執行緒,主執行緒繼續迴圈監聽其他連線請求,而不用停留在那裡單獨為這一個客戶端服務。
與此同時我們還要考慮一些其他問題,是不是每個客戶端連線上來我們都要給他建立一個執行緒呢。顯然是不能的,因為系統的socket資源是有限的,執行緒資源也是有限的。而且每新增一個客戶端,就會增多記憶體的開銷,我們還要考慮記憶體是否夠用。所以要根據裝置的實際能力來判斷他的最大連線數量(當然如果只是為了瞭解原理,實現基本功能,將最大連線數設定為幾百上千個對於一般機器來言是沒有任何問題的)。也正是因為系統資源的寶貴,我們還要做好回收工作。比如不用的socket我們要及時close,執行緒結束後還要及時關閉控制代碼。
另外一點,我們還要掌握所有客戶端執行緒的控制權,所以我們要有一個結構體來儲存這些客戶端的基本資訊,包括他所佔執行緒的ID,套接字等等以及一些實用性的屬性。就好像你要寫一個csdn的伺服器,你不僅要記錄執行緒id和套接字,你還要記錄使用者名稱、會員等資訊。
我的多執行緒是用Windows的控制代碼建立的。但我個人建議還是使用pthread。因為C語言在linux上的使用非常廣泛。用pthread寫執行緒函式可移植性更強,跨平臺的時候修改的地方更少。這裡我先使用windows控制代碼來實現程式碼。以後會都使用pthread。
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#define MAX_CLIENT_NUMS 999
#pragma comment(lib,"ws2_32.lib")
typedef struct client_list_node
{
SOCKET socket_client; //客戶端的socket
struct sockaddr_in c_sin; //用於儲存已連線的客戶端的socket基本資訊
int is_run; //標記這個節點的socket是否正在被使用
HANDLE h; //為這個socket建立的執行緒 的控制代碼
}client_list_node_st, *client_list_node_t;
static client_list_node_st client_list[MAX_CLIENT_NUMS] = {0}; //客戶端列表
static SOCKET socket_of_server; //服務端(本地)的socket
static struct sockaddr_in s_sin; //用於儲存本地建立socket的基本資訊
static int client_nums = 0; //當前連線伺服器的客戶端的個數
static void analysis(char* data, int datal, client_list_node_t node_t);
DWORD WINAPI myfun1(LPVOID lpParameter); //宣告執行緒函式
int main(int argc, char* argv[])
{
int port = 6666;
int i = 0;
WORD socket_version = MAKEWORD(2, 2);
WSADATA wsadata;
if (WSAStartup(socket_version, &wsadata) != 0)
{
return 0;
}
if (argc > 1) //埠號可以通過啟動引數配置,沒有配置啟動引數的時候預設埠6666
{
port = atoi(argv[1]);
}
socket_of_server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//建立socket 並判斷是否建立成功
if (socket_of_server == INVALID_SOCKET)
{
printf("socket error\n");
return 0;
}
s_sin.sin_family = AF_INET; //定義協議族為IPV4
s_sin.sin_port = htons(port);//規定埠號
s_sin.sin_addr.S_un.S_addr = INADDR_ANY;
/************************************************************************
s_sin.sin_addr.S_un.S_addr = INADDR_ANY; 是在設定繫結在本機的哪個IP地址上,
在哪個IP地址上進行監聽。設定為INADDR_ANY代表0.0.0.0就是預設IP。
正常個人程式設計時 這個地方無關緊要。但若真正應用時這個地方最好設定清楚。
因為好多伺服器是多個網絡卡,本機有多個IP。哪個網絡卡是連線服務所在區域網的就要
設定為哪個。
************************************************************************/
if (bind(socket_of_server, (LPSOCKADDR)&s_sin, sizeof(s_sin)) == SOCKET_ERROR)//繫結
{
printf("bind error\n");
}
if (listen(socket_of_server, 5) == SOCKET_ERROR)//監聽
{
printf("listen error\n");
return 0;
}
printf("服務已經開啟 PORT:%d,正在等待連線... ...\n", port);
while (1)
{
SOCKET socket_of_client; //客戶端(遠端)的socket
struct sockaddr_in c_sin; //用於儲存已連線的客戶端的socket基本資訊
int c_sin_len; //函式accept的第三個引數,c_sin的大小。
c_sin_len = sizeof(c_sin);
socket_of_client = accept(socket_of_server, (SOCKADDR *)&c_sin, &c_sin_len);
/************************************************************************
沒有新的連線是 程式不會一直在這裡迴圈。此時accept會處於阻塞狀態。
直到有新的連線,或者出現異常。
************************************************************************/
if (socket_of_client == INVALID_SOCKET)
{
printf("accept error\n");
continue; //繼續等待下一次連線
}
else
{
if (client_nums + 1 > MAX_CLIENT_NUMS)
{
send(socket_of_client, "連線超限制,您已被斷開 \n", strlen("連線超限制,您已被斷開 \n"), 0);
printf("有新的客戶端請求連入IP:%s PORT:%d,但由於伺服器已經達到最大連線數,該裝置已經被強行斷開\n", inet_ntoa(c_sin.sin_addr), c_sin.sin_port);
Sleep(1000);
closesocket(socket_of_client);
continue;
}
else
{
int j = 0;
for (j = 0; j < MAX_CLIENT_NUMS; j++)
{
if (client_list[j].is_run == 0)
{
client_list[j].is_run = 1;
client_list[j].socket_client = socket_of_client;
client_list[j].c_sin;
memcpy(&(client_list[j].c_sin), &c_sin, sizeof(c_sin));
if (client_list[j].h)
{
CloseHandle(client_list[j].h);
}
client_list[j].h = CreateThread(NULL, 0, myfun1, &(client_list[j]), 0, NULL);
client_nums++;
break;
}
}
}
}
}
closesocket(socket_of_server);
WSACleanup();
return 0;
}
static void analysis(char* data, int datal, client_list_node_t node_t)
{
printf("客戶端(%s:%d)發來資料:%s 資料長度:%d\n", inet_ntoa(node_t->c_sin.sin_addr), node_t->c_sin.sin_port, data, datal);
//在這裡我們可以對已接收到的資料進行處理
//一般情況下這裡都是處理“粘包”的地方
//解決粘包之後 將完整的資料傳送給資料處理函式
}
DWORD WINAPI myfun1(LPVOID lpParameter)
{
char revData[256];//這個地方一定要酌情設定大小,這決定了每次能獲取多少資料
int ret;//recv函式的返回值 有三種狀態每種狀態的含義在下方有解釋
client_list_node_t node = (client_list_node_t)lpParameter;
printf("有新客戶端連入伺服器 , IP = %s PORT = %d \n", inet_ntoa(node->c_sin.sin_addr), node->c_sin.sin_port);
printf("最多可連線%d個客戶端,當前已連線%d個客戶端\n", MAX_CLIENT_NUMS, client_nums);
send(node->socket_client, "hello i am server \n", strlen("hello i am server \n"), 0);
while (1)
{
//接收來自 這個客戶端的訊息
ret = recv(node->socket_client, revData, 255, 0);
/************************************************************************
recv函式 的實質就是從socket的緩衝區裡拷貝出資料
返回值就是拷貝出位元組數的大小。
上面定義的載體(revData)大小是255,所以recv的第三個引數最大隻能設定為255,
如果設定為大於255的數值,當執行recv函式時恰好緩衝區的內容大於255,
就會導致記憶體洩漏,導致ret值小於零,解除阻塞狀態。因此這裡最好將第三個引數
設定為revData的大小,那麼當緩衝區內的資料小於255的時候
只需要執行一次recv就可以將緩衝區的內容都拷貝出來,但當緩衝區的資料大
於255的時候,就要執行多次recv函式。當緩衝區內沒有內容的時候,會處於阻塞
狀態,這個while函式會停在這裡。直到新的資料進來或者出現異常。
************************************************************************/
if (ret > 0)
{
revData[ret] = 0x00;//正常情況下不必這麼做,我這麼做只是為了能按字串的形式輸出它
analysis(revData, ret, node);
}
else if (ret == 0)
{
//當ret == 0 說明客戶端已斷開連線。這裡我們直接跳出迴圈去等待下次連線即可。
printf("客戶端斷開連線, IP = %s PORT = %d \n", inet_ntoa(node->c_sin.sin_addr), node->c_sin.sin_port);
closesocket(node->socket_client);
break;
}
else//ret < 0
{
//當ret < 0 說明出現了異常 例如阻塞狀態解除,或者讀取資料時出現指標錯誤等。
//所以我們這裡要主動斷開和客戶端的連結,然後跳出迴圈去等待新的連線。
printf("在與客戶端通訊是發生異常 IP = %s PORT = %d \n", inet_ntoa(node->c_sin.sin_addr), node->c_sin.sin_port);
closesocket(node->socket_client);
break;
}
}
node->is_run = 0;
client_nums--;
printf("最多可連線%d個客戶端,當前已連線%d個客戶端\n", MAX_CLIENT_NUMS, client_nums);
return;
}
上面程式碼親測有效可以直接拷貝走執行,仔細看我的註釋,不難理解。
當然這段程式碼只可以簡單使用,還是有核心問題沒解決的。這就涉及到互斥量的問題了。
每增加一個客戶端當前的連線數量就會增加一,程式碼中我們會執行client_nums++,客戶端斷開連線時會執行client_num–。但是假如同時有很多客戶單斷開連線,那麼這麼多執行緒要同時訪問client_nums的記憶體,執行自減。這就會出現意料之外的效果。這和計算機工作原理有關係,當client_nums執行自減運算的時候,系統會將變數的值取走,然後在cpu內進行計算,計算好了之後在將結果儲存在client_nums的記憶體中。舉個實際的例子來解釋意外的效果是怎麼產生的。假設系統當前連線客戶端5個那麼client_nums的值就是5,兩個客戶端同時斷開連線,此時兩個執行緒a和b幾乎要同時執行client_nums的自減運算,理想的情況是一個執行緒先執行完client_nums的自減運算,另一個執行緒繼續執行自減,client_nums就會得到理想值3,但是還有另外一種可能,執行緒a取走了client_nums的值,還沒等運算完成將結果存到原地址內之前,執行緒b也取走了client_nums的值,那麼執行緒a和執行緒b取走的都是5。經過執行緒a的計算將得到了結果為4儲存在了client_nums的地址裡,而執行緒b的運算結果也是4。此時client_nums的值就是4。但實際連線的數量只有3個。
就上述程式碼而言兩個執行緒同時訪問一塊記憶體的情況還會出現在其他地方,比如當有客戶端連線的時候我們需要client_nums執行自加預算,但恰好此時有客戶端斷開連線,那麼又會出現兩個執行緒同時訪問一塊記憶體,一個是自加,一個是自減。
基於此基礎我們引入了互斥鎖的概念。能很好的解決這個問題,具體內容後面的文章會解釋,這裡不詳述。