首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C語言網絡通信編程:Socket 基礎與完整 Client/Server 範例

     編輯:關於C語言

核心結構體速覽

Windows Socket 程式設計離不開三個核心結構體,理解它們的記憶體佈局與轉換關係是寫出正確網路代碼的前提。

結構體用途關鍵欄位
sockaddr 通用 Socket 位址結構,供 API 參數使用 sa_family (地址族)、sa_data[14] (原始位址資料)
sockaddr_in IPv4 專用,填寫方便 sin_family、sin_port (網路序)、sin_addr (in_addr)、sin_zero[8] (填充對齊)
in_addr 32 位 IPv4 位址 S_un.S_addr (u_long)

使用規則:實際填充時使用 sockaddr_in,呼叫 bind/connect/accept 時強制轉型為 (sockaddr*)。這是歷史遺留設計,sockaddr 早期定義過大,後來為方便 IPv4 填寫才引入 sockaddr_in,兩者記憶體大小相同。

位序轉換與位址字串轉換

  • 主機序 ⇄ 網路序:htons/htonl (Host→Network),ntohs/ntohl (Network→Host)。x86 為小端序,網路標準為大端序,**跨機器通訊務必轉換**。
  • 字串 ⇄ 二進位 IP:inet_addr("192.168.1.1") → u_long;inet_ntoa(in_addr) → char* (靜態緩衝區,非執行緒安全)。

核心 Socket API 流程圖解

Server: socket() → bind() → listen() → accept() → send/recv → closesocket()
Client: socket() → connect() → send/recv → closesocket()
函式關鍵參數常見誤區
socketAF_INET, SOCK_STREAM, IPPROTO_TCP回傳 INVALID_SOCKET 表失敗,非 0
bind本地 IP + PortServer 必須 bind;Client 通常不需顯式 bind(connect 會隱式綁定暫時埠)
listenbacklog (待處理佇列上限)常用 SOMAXCONN 讓系統決定
accept輸出參數回傳 Client 位址**阻塞**直到有連線;回傳**新 Socket** 專供該 Client 通訊
connect目標 IP + Port內部會自動挑選可用埠並 bind

完整範例:阻塞式 TCP Echo Server / Client

以下代碼已整理為可直接編譯的單檔版本,採用 WS2_32.lib,適用於 Visual Studio 或 MinGW-w64。

Server 端

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")
#define PORT 4600
#define BACKLOG SOMAXCONN

int main() {
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) {
        fprintf(stderr, "WSAStartup 失敗:%d\n", WSAGetLastError());
        return 1;
    }

    SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (listenSock == INVALID_SOCKET) {
        fprintf(stderr, "socket 失敗:%d\n", WSAGetLastError());
        WSACleanup(); return 1;
    }

    sockaddr_in svr = {0};
    svr.sin_family = AF_INET;
    svr.sin_addr.s_addr = INADDR_ANY;          // 監聽所有介面
    svr.sin_port = htons(PORT);

    if (bind(listenSock, (sockaddr*)&svr, sizeof(svr)) == SOCKET_ERROR) {
        fprintf(stderr, "bind 失敗:%d\n", WSAGetLastError());
        closesocket(listenSock); WSACleanup(); return 1;
    }

    if (listen(listenSock, BACKLOG) == SOCKET_ERROR) {
        fprintf(stderr, "listen 失敗:%d\n", WSAGetLastError());
        closesocket(listenSock); WSACleanup(); return 1;
    }

    printf("Server 啟動,監聽埠 %d ...\n", PORT);

    while (1) {
        sockaddr_in cli = {0};
        int cliLen = sizeof(cli);
        SOCKET connSock = accept(listenSock, (sockaddr*)&cli, &cliLen);
        if (connSock == INVALID_SOCKET) {
            fprintf(stderr, "accept 失敗:%d\n", WSAGetLastError());
            continue;
        }
        printf("Client 連入:%s:%d\n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port));

        char buf[1024];
        int n = recv(connSock, buf, sizeof(buf)-1, 0);
        if (n > 0) {
            buf[n] = '\0';
            printf("收到:%s\n", buf);
            send(connSock, buf, n, 0); // 原樣回傳
        }
        closesocket(connSock);
    }

    closesocket(listenSock);
    WSACleanup();
    return 0;
}

Client 端

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")
#define SERVER_IP "127.0.0.1"
#define PORT 4600

int main() {
    WSADATA wsa;
    if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) return 1;

    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET) { WSACleanup(); return 1; }

    sockaddr_in svr = {0};
    svr.sin_family = AF_INET;
    svr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &svr.sin_addr); // 比 inet_addr 更現代

    if (connect(sock, (sockaddr*)&svr, sizeof(svr)) == SOCKET_ERROR) {
        fprintf(stderr, "connect 失敗:%d\n", WSAGetLastError());
        closesocket(sock); WSACleanup(); return 1;
    }

    const char* msg = "Hello from Client!";
    send(sock, msg, (int)strlen(msg), 0);

    char buf[1024];
    int n = recv(sock, buf, sizeof(buf)-1, 0);
    if (n > 0) { buf[n] = '\0'; printf("Server 回應:%s\n", buf); }

    closesocket(sock);
    WSACleanup();
    return 0;
}

實戰避坑指南

  1. WSAStartup / WSACleanup 必須成對呼叫,且版本請求 MAKEWORD(2,2)。
  2. send/recv 回傳值檢查:send 可能發送不完整,recv 可能分多次讀取;生產環境需實作迴圈收發。
  3. 阻塞模式下 accept/recv 會卡住執行緒,GUI 程式請改用 select/WSAAsyncSelect/IOCP 非同步模型。
  4. 位序轉換不可省略:htons、htonl、inet_pton 皆為必須。
  5. 錯誤碼取用 WSAGetLastError(),而非 GetLastError()。

延伸閱讀

  • 《Unix Network Programming》Vol.1 – 經典理論基石
  • MSDN: Windows Sockets 2 (Winsock)
  • Beej's Guide to Network Programming – 跨平台 Socket 入門首選
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved