初识 Socket 网络编程

简单介绍了 Socket 网络编程的常用接口,以及它们在 TCP 协议中的调用过程,并包含代码示例

什么是 Socket ?

Socket(套接字)被定义为在网络中传输信息的唯一标识。例如,我们通常将 Socket 定义为 IP 地址和端口的组合,因为一个 IP 地址和端口可以唯一确定一个网络通信端点。Socket 是一个较为抽象的概念,在 “一切皆文件” 的思想下,我们可以像操作一个 FD(文件描述符)一样操作一个 Socket,通过它来收发网络数据,实现网络通信。

Socket 网络编程

Socket 网络编程是一种用于实现网络通信的编程技术。它基于 Socket API,通过创建 Socket 来实现不同计算机或进程之间的数据传输和通信。在 Socket 网络编程中,有两个关键的角色:Server(服务端)和 Client(客户端),Server 负责监听和接受 Client 的连接请求,而 Client 负责发起连接请求并与 Server 进行通信。Socket 网络编程使用 TCP/IP 协议(传输控制协议/因特网协议),TCP 协议提供可靠的、面向连接的通信,适用于要求数据传输的完整性和可靠性的场景。同时也可以使用 UDP 协议(用户数据报协议),UDP 协议提供无连接的通信,适用于对实时性要求较高的场景。

本文主要认识 TCP/IP 协议下的 Socket 网络编程。

Socket API

在介绍具体的 Socket API 前我们需要分别了解在 Socket 网络编程中 Server 和 Client 分别需要哪些操作:

  1. Server 创建一个 Socket,并绑定到指定的 IP 和端口,开始监听 Client 的连接请求
  2. Client 创建一个 Socket,并指定 Server 的 IP 和端口,发起连接请求
  3. Server 接受客户端的连接请求,并创建一个新的 Socket 与 Client 进行通信
  4. Client 和 Server 通过收发数据进行通信
  5. 通信完成后,Client 和 Server 分别关闭 Socket

API 调用过程如下图所示:

Socket API TCP 调用过程

socket()

socket() 用于创建一个新的 Socket,返回一个文件描述符,用于后续的操作。函数原型如下:

1
int socket(int domain, int type, int protocol);
  • domain 指定协议族,有 IPv4, IPv6, UNIX。
  • type 指定 Socket 类型,有面向连接的可靠字节流 Socket,无连接的不可靠数据报 Socket。
  • protocol 指定协议,一般为 0 即根据 domaintype 选择默认协议。

bind()

bind() 用于将一个 Socket 与特定的地址(IP + Port)进行绑定。函数原型如下:

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • addr 指向需要绑定的地址。

listen()

listen() 用于使一个 Socket 进入监听状态。函数原型如下:

1
int listen(int sockfd, int backlog);
  • backlog 表示等待连接队列的最大长度。

accept()

accept() 用于接收客户端连接请求。函数原型如下:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • addr 用于存储接受连接请求的 Client 的地址信息

该函数调用会导致 Server 等待并接受来自 Client 的连接请求,当有新的连接请求到达后,accept() 会创建一个新的 Socket 并返回。通过这个新的 Socket 与 Client 通信。

connect()

connect() 用于在 Socket 上建立与 Server 的连接,其中涉及到的 TCP 三次握手暂且不表。函数原型如下:

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • addr 指定了 Server 的地址

send() & recv()

send() & recv() 分别用于发送与接收数据。函数原型如下:

1
2
3
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • buf 指向发送或接收缓冲区
  • len 希望发送或接收的数据字节数
  • flags 可选的标志参数,用于修改默认行为

返回值为实际发送或接收的数据字节数。

close()

close() 用于关闭 Socket,释放相关资源。函数原型如下:

1
int close(int fd);

完整代码实现

Server

 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
// tcp-server-demo.cc

#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    // 创建IPv4 TCP Socket
    int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        std::cout << "Failed to create socket, error message: " << strerror(errno) << std::endl;
        return -1;
    }

    sockaddr_in server_addr = {};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(10086); // 指定端口号为10086
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 接收任何地址的连接

    if (bind(server_sock, (const sockaddr *)&server_addr, sizeof(sockaddr_in)) == -1) {
        std::cout << "Failed to bind socket, error message: " << strerror(errno) << std::endl;
        close(server_sock);
        return -1;
    }

    if (listen(server_sock, 10) == -1) {
        std::cout << "Failed to listen socket, error message: " << strerror(errno) << std::endl;
        close(server_sock);
        return -1;
    }

    sockaddr_in client_addr = {};
    socklen_t client_addrlen = sizeof(client_addr);
    // 接受Client连接并创建新Socket
    int client_sock = accept(server_sock, (sockaddr *)&client_addr, &client_addrlen);
    if (client_sock == -1) {
        std::cout << "Failed to accept client, error message: " << strerror(errno) << std::endl;
        close(server_sock);
        return -1;
    }

    char buffer[1024] = {0};
    // 接收Client发送的数据
    ssize_t bytes_recv = recv(client_sock, buffer, sizeof(buffer) - 1, 0);
    if (bytes_recv == -1) {
        std::cout << "Failed to receive data from client, error message: " << strerror(errno) << std::endl;
        close(client_sock);
        close(server_sock);
        return -1;
    }

    std::cout << "Received message from client: " << buffer << std::endl;

    // 返回数据给Client
    const char *respone_msg = "Hello, here is server.";
    ssize_t bytes_sent = send(client_sock, respone_msg, strlen(respone_msg), 0);
    if (bytes_sent <= 0) {
        std::cout << "Failed to send data to client, error message: " << strerror(errno) << std::endl;
        close(client_sock);
        close(server_sock);
        return -1;
    }

    std::cout << "Response hello to client" << std::endl;

    close(client_sock);
    close(server_sock);

    return 0;
}

Client

 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
// tcp-client-demo.cc

#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    int client_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (client_sock == -1) {
        std::cout << "Failed to create socket, error message: " << strerror(errno) << std::endl;
        return -1;
    }

    sockaddr_in server_addr = {};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(10086);
    server_addr.sin_addr.s_addr = inet_addr("172.21.2.127"); // WSL中的地址

    if (connect(client_sock, (const sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        std::cout << "Failed to connect to server, error message: " << strerror(errno) << std::endl;
        close(client_sock);
        return -1;
    }

    const char *hello_msg = "Hello, here is client";
    ssize_t bytes_sent = send(client_sock, hello_msg, strlen(hello_msg), 0);
    if (bytes_sent <= 0) {
        std::cout << "Failed to send data to server, error message: " << strerror(errno) << std::endl;
        close(client_sock);
        return -1;
    }

    char buffer[1024] = {0};
    ssize_t bytes_recv = recv(client_sock, buffer, sizeof(buffer), 0);
    if (bytes_recv == -1) {
        std::cout << "Failed to receive data from server, error message: " << strerror(errno) << std::endl;
        close(client_sock);
        return -1;
    }

    std::cout << "Received message from server: " << buffer << std::endl;

    close(client_sock);

    return 0;
}

运行结果

1
2
3
4
# Server
➜  build git:(main) ✗ ./server
Received message from client: Hello, here is client
Response hello to client
1
2
3
# Client
➜  build git:(main) ✗ ./client
Received message from server: Hello, here is server.

小结

本文粗略介绍了一下 Socket 网络编程的概念,TCP 协议下 Socket 编程的 API 以及调用过程,并没有进入到协议内部的具体细节,展示了一个简单到不能再简单的 demo 演示 Server 和 Client 代码实现,仅仅是对 Socket 网络编程有个初步的认知。

参考

  1. ChatGPT

  2. RFC 147 - Definition of a socket