C/C++ Windows Socket (Winsock) TCP Socket Server/Client 網路通訊教學

本篇 ShengYu 介紹如何寫 C/C++ Windows Socket TCP Socket Server/Client 網路通訊程式,Windows Socket 簡稱 Winsock,在這個網路盛行的時代,網路通訊已成為基礎,想要精通學習網路通訊必須先了解 TCP/IP 協定,其中又以 TCP 通訊最常被使用,TCP 通訊程式通常分成伺服器端與客戶端的兩部份程式,接下來教學內容將介紹如何使用 socket API 來搭建一個典型的 TCP 通訊程式,甚至可以寫出一個聊天室的程式,或者像 LINE 這樣的通訊程式。

以下 C/C++ Winsock TCP 內容將分為幾部分,分別為:

  • 常見的 Socket API 函式 Overview 總覽
  • C/C++ Winsock Socket TCP Server/Client 通訊流程
  • C/C++ Winsock TCP Server 伺服器端程式 (Echo Sever)
  • C/C++ Winsock TCP Client 客戶端程式 (傳送使用者的輸入)
  • C/C++ Winsock TCP Client 客戶端程式 (定時傳送資料)
  • C/C++ Winsock TCP 常見問題
  • Winsock 的 ip 字串轉 sockaddr_in
  • Winsock 的 sockaddr_in 轉 ip 字串

常見的 Socket API 函式 Overview 總覽

C/C++ Winsock 的 socket 模組它提供了標準的 BSD Socket API,主要的 socket API 函式如下:
socket():建立 socket 與設定使用哪種通訊協定
bind(sock_fd, addr):將 socket 綁定到地址
listen(sock_fd, n):開始監聽 TCP 傳入連接,n 指定在拒絕連線前,操作系統可以掛起的最大連接數,該值最少為1,通常設為5就夠用了
accept(sock_fd, addr):等待連線,接受到 TCP 連線後,可以從 addr 得知連線客戶端的地址。
connect(address):連線到 address 處的 socket
recv():接收 TCP 資料
send():發送 TCP 資料
closesocket():關閉 socket

C/C++ Winsock Socket TCP Server/Client 通訊流程

以下 ShengYu 講解 C/C++ Winsock TCP Server 端與 TCP Client 端的程式流程以及會如何使用這些 socket API,
TCP Server 的流程分為以下幾大步驟:

  1. 建立socket:sock_fd = socket(AF_INET, SOCK_STREAM, 0);,指定 AF_INET (Internet Protocol) family 的通訊協定,類型使用 SOCK_STREAM (Stream Socket) 也就是 TCP 傳輸方式
  2. 綁定 socket 到本地 IP 與 port:bind(sock_fd, ...)
  3. 開始監聽:listen(sock_fd, ...)
  4. 等待與接受客戶端的請求連線:new_fd = accept(sock_fd, ...)
  5. 接收客戶端傳來的資料:recv(new_fd, ...)
  6. 傳送給對方發送資料:send(new_fd, ...)
  7. 傳輸完畢後,關閉 socket:closesocket(new_fd)

TCP Client 的流程分為以下幾大步驟:

  1. 建立 socket:sock_fd = socket(AF_INET, SOCK_STREAM, 0);
  2. 連線至遠端地址:connect(sock_fd, ...)
  3. 傳送資料:send(sock_fd, ...)
  4. 接收資料:recv(sock_fd, ...)
  5. 傳輸完畢後,關閉 socket:closesocket(sock_fd)

以上是 TCP Server/Client 通訊的重點流程,實際的 C/C++ Winsock socket API 用法與範例詳見下列章節,接下來就來看看怎麼寫 TCP Server/Client 通訊程式吧!

C/C++ Winsock TCP Server 伺服器端程式 (Echo Sever)

這邊 ShengYu 就開始介紹怎麼寫 C/C++ Winsock TCP Server 程式,下列範例這是一個典型的 Echo Server,Echo Server 就是收到什麼資料就回覆什麼資料,很簡單吧!
跟網路上其他範例不同的是此範例建立連線後不是傳輸一次資料就關閉連線,而是使用迴圈可以一直傳輸資料直到客戶端不想傳關閉連線為止,並且伺服器端再次地等待新的客戶端連線來服務。

要使用 Winsock 的話要 include winsock2.h 標頭檔,winsock2.h 是用來取代 winsock.h,除非你要使用 Winsock 1.1 否則你應該使用 winsock2.h 標頭檔,winsock.h 要連結的函式庫為 wsock32.lib,而 winsock2.h 要連結為 Ws2_32.lib 函式庫。

另外一點要注意的是 windows.h 預設會 include winsock.h,所以如果你在 windows.h 後面在 include winsock2.h 的話會編譯錯誤,原因是因為 winsock.h 跟 winsock2.h 這兩個不應該同時存在,winsock2.h 的設計是用來取代 winsock.h 的而不是擴充的概念,所以正確做法會是 winsock2.h 要在 windows.h 之前被 include。

跟 linux socket 相比,winsock 需要在最一開始要呼叫 WSAStartup 函式,以及最後要呼叫 WSACleanup 函式。

如下例所示,伺服器端一開始建立 socket,用 bind() 綁定,這裡是使用 0.0.0.0, port 為 7000
使用 listen() 開始監聽,上限連線數為5,之後進入主迴圈,accept() 等待接受客戶端的連線請求,
一旦有客戶端連線的話,就會從 accept() 繼續往下執行,
之後是另一個迴圈來服務這個連線,不斷地從這個連線 recv 接收資料與 send 傳送資料,
如果 recv() 的回傳值為0,表示客戶端已斷開連線,此時我們也關閉這個連線,
之後回到 accept() 等待新的客戶端連線,等到新的客戶端連線連上便跟之前的流程一樣,這樣便是一個完整的 C/C++ Winsock TCP 伺服器程式。

cpp-windows-tcp-socket-server.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

const char* host = "0.0.0.0";
int port = 7000;

int main()
{
SOCKET sock, new_sock;
socklen_t addrlen;
struct sockaddr_in my_addr, client_addr;
int status;
char indata[1024] = {0}, outdata[1024] = {0};
int on = 1;

// init winsock
WSADATA wsa = { 0 };
WORD wVer = MAKEWORD(2, 2);
WSAStartup(wVer, &wsa);
if (WSAStartup(MAKEWORD(2, 2), &wsa) != NO_ERROR) {
printf("Error: init winsock\n");
exit(1);
}

// create a socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("Socket creation error");
exit(1);
}

// for "Address already in use" error message
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(int)) == -1) {
perror("Setsockopt error");
exit(1);
}

// server address
my_addr.sin_family = AF_INET;
inet_pton(AF_INET, host, &my_addr.sin_addr);
my_addr.sin_port = htons(port);

status = bind(sock, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (status == -1) {
perror("Binding error");
exit(1);
}
char my_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &my_addr.sin_addr, my_ip, sizeof(my_ip));
printf("server start at: %s:%d\n", my_ip, port);

status = listen(sock, 5);
if (status == -1) {
perror("Listening error");
exit(1);
}
printf("wait for connection...\n");

addrlen = sizeof(client_addr);

while (1) {
new_sock = accept(sock, (struct sockaddr *)&client_addr, &addrlen);
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("connected by %s:%d\n", client_ip, ntohs(client_addr.sin_port));

while (1) {
int nbytes = recv(new_sock, indata, sizeof(indata), 0);
if (nbytes <= 0) {
closesocket(new_sock);
printf("client closed connection.\n");
break;
}
printf("recv: %s\n", indata);

sprintf(outdata, "echo %s", indata);
send(new_sock, outdata, strlen(outdata), 0);
}
}
closesocket(sock);
WSACleanup();

return 0;
}

如果 Server 伺服器端不正常關閉後再次啟動時可能會遇到 Binding error: Address already in use 這種錯誤訊息的話,那麼你可以在 bind() 之前設定 REUSEADDR 可以解決這個問題,

1
2
int on = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(int));

C/C++ Winsock TCP Client 客戶端程式 (傳送使用者的輸入)

先用一個終端機來啟動前述的 TCP 伺服器端的程式,接著再用另一個終端機執行 TCP 客戶端的程式。C/C++ Winsock TCP Client 範例如下,這邊要示範的是傳送使用者的輸入訊息,將使用者的輸入訊息傳送給伺服器端,通常應用於一般聊天軟體上,學習之後就可以寫一個簡單的聊天軟體了。

如下例所示,客戶端一開始建立 socket,之後 connect() 連線伺服器主機的 host 與 port,
之後進入主迴圈,不斷地傳送使用者的輸入,這邊是使用 gets_s() 取得使用者輸入的資料,也可以使用 scanf()fgets() 等函式,
使用者輸入完後按下 Enter 便會將資料發送給伺服器端,接著等待伺服器端傳送資料,接收到來自伺服器端的資料就把它印出來,

cpp-windows-tcp-socket-client.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

const char* host = "127.0.0.1";
int port = 7000;

int main()
{
SOCKET sock;
struct sockaddr_in serv_name;
int status;
char indata[1024] = {0}, outdata[1024] = {0};

// init winsock
WSADATA wsa = { 0 };
WORD wVer = MAKEWORD(2, 2);
WSAStartup(wVer, &wsa);
if (WSAStartup(MAKEWORD(2, 2), &wsa) != NO_ERROR) {
printf("Error: init winsock\n");
exit(1);
}

// create a socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("Socket creation error");
exit(1);
}

// server address
serv_name.sin_family = AF_INET;
inet_pton(AF_INET, host, &serv_name.sin_addr);
serv_name.sin_port = htons(port);

status = connect(sock, (struct sockaddr *)&serv_name, sizeof(serv_name));
if (status == -1) {
perror("Connection error");
exit(1);
}

while (1) {
printf("please input message: ");
gets_s(outdata);
printf("send: %s\n", outdata);
send(sock, outdata, strlen(outdata), 0);

int nbytes = recv(sock, indata, sizeof(indata), 0);
if (nbytes <= 0) {
closesocket(sock);
printf("server closed connection.\n");
break;
}
printf("recv: %s\n", indata);
}
WSACleanup();

return 0;
}

以下示範一下程式的啟動過程,過程中我在客戶端輸入了兩次的訊息,最後按 ctrl+c 結束了程式,
客戶端輸出結果如下,

client
1
2
3
4
5
6
7
8
9
> client.exe
please input message: hello
send: hello
recv: echo hello
please input message: hello tcp
send: hello tcp
recv: echo hello tcp
please input message: send:
^C

伺服器端輸出結果如下,

server
1
2
3
4
5
6
7
> server.exe
server start at: 0.0.0.0:7000
wait for connection...
connected by 127.0.0.1:25886
recv: hello
recv: hello tcp
client closed connection.

C/C++ Winsock TCP Client 客戶端程式 (定時傳送資料)

前一章節示範了 Echo Sever 與 Client 通訊程式,這時可以打鐵趁熱,除了前一章節 TCP Client 使用者手動輸入的情形之外,這邊也介紹另一種客戶端會定時地傳送資料給伺服器端,同時這也適用於各種通訊情形。

步驟跟前一章節 TCP Client 幾乎相同,傳輸字串為 'heartbeat',這邊傳送後使用 Sleep(1000) 來讓程式睡眠1秒,之後再繼續傳送資料,進而達成定時傳送的功能,

cpp-windows-tcp-socket-client-heartbeat.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

const char* host = "127.0.0.1";
int port = 7000;

int main()
{
SOCKET sock;
struct sockaddr_in serv_name;
int status;
char indata[1024] = {0}, outdata[1024] = {0};

// init winsock
WSADATA wsa = { 0 };
WORD wVer = MAKEWORD(2, 2);
WSAStartup(wVer, &wsa);
if (WSAStartup(MAKEWORD(2, 2), &wsa) != NO_ERROR) {
printf("Error: init winsock\n");
exit(1);
}

// create a socket
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("Socket creation error");
exit(1);
}

// server address
serv_name.sin_family = AF_INET;
inet_pton(AF_INET, host, &serv_name.sin_addr);
serv_name.sin_port = htons(port);

status = connect(sock, (struct sockaddr *)&serv_name, sizeof(serv_name));
if (status == -1) {
perror("Connection error");
exit(1);
}

while (1) {
strcpy(outdata, "heartbeat");
printf("send: %s\n", outdata);
send(sock, outdata, strlen(outdata), 0);

int nbytes = recv(sock, indata, sizeof(indata), 0);
if (nbytes <= 0) {
closesocket(sock);
printf("server closed connection.\n");
break;
}
printf("recv: %s\n", indata);

Sleep(1000);
}
WSACleanup();

return 0;
}

客戶端輸出結果如下,

client
1
2
3
4
5
6
7
8
9
10
11
12
> client.exe
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
^C

伺服器端輸出結果如下,

server
1
2
3
4
5
6
7
8
9
10
> server.exe
server start at: 0.0.0.0:7000
wait for connection...
connected by 127.0.0.1:25910
recv: heartbeat
recv: heartbeat
recv: heartbeat
recv: heartbeat
recv: heartbeat
client closed connection.

C/C++ Winsock TCP 常見問題

在 TCP 的傳輸裡,為什麼伺服器還要回傳給客戶端?
因為這只是個示範用的通訊程式,讓你了解通訊的過程,就像打電話或者跟別人對話一樣,你一句我一句的來回互動,你可以根據實際的需求而修改程式,你也可以改成一直傳,例如客戶端一直傳送,伺服器一直接收。

為什麼 recv 還沒收到資料前會卡住一直等?
因為預設是 blocking 非阻塞模式,recv 還沒收到資料前會卡住一直等,沒法做其他事情,直到 recv 接收到資料才會從 recv 函式返回,解決辦法是改用 Non-blocking 非阻塞模式,Non-blocking 模式是這次沒接收到資料就會從 recv 函式返回,接著繼續往下執行;另一個解決方式是另外建立執行緒去做其他事情。

Winsock 的 ip 字串轉 sockaddr_in

在 linux 中 ip 字串轉換成 sockaddr_in 結構可用 inet_aton (ipv4 only)跟 inet_pton (ipv4 & ipv6) 兩個 API 達成,但在 windows 中沒有 inet_aton,而是 inet_addr (ipv4) 可用,
inet_addr 用法如下,

1
2
3
const char* host = "0.0.0.0";
struct sockaddr_in my_addr;
my_addr.sin_addr.s_addr = inet_addr(host);

但現代的 MS 編譯器都出現 C4996 編譯錯誤,內容如下,

1
'inet_addr': Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

除非你要使用 define _WINSOCK_DEPRECATED_NO_WARNINGS,否則我建議改使用 inet_pton,使用 inet_pton 的話好處就是 windows 跟 linux 都可以通用,跨平台時程式碼修改的比較少,所以基本上在 windows 平台中使用 inet_pton 就對了!
inet_pton 使用方法如下,

1
2
3
const char* host = "0.0.0.0";
struct sockaddr_in my_addr;
inet_pton(AF_INET, host, &my_addr.sin_addr); // AF_INET -> ipv4

Winsock 的 sockaddr_in 轉 ip 字串

承上節,在 Winsock 中 要將 sockaddr_in 結構轉換成 ip 字串的話可用 inet_ntop (ipv4 & ipv6) API 達成,
inet_ntop 用法如下,

1
2
3
struct sockaddr_in client_addr;
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));

以上就是 C/C++ Windows Socket (Winsock) TCP Socket Server/Client 網路通訊教學,
如果你覺得我的文章寫得不錯、對你有幫助的話記得 Facebook 按讚支持一下!