简单 TCP 服务器和客户端程序

简单的 TCP 服务端和客户端

就像我们如何开始任何编程语言一样,我们将在套接字编程中实现一个简单的 hello world 程序。

在本文中,我们将实现一个简单的 TCP 服务端和客户端。

  1. 服务端将在特定的端口和 IP 地址上监听。
  2. 客户端将连接到服务端并向服务端发送数据。
  3. 服务端接收到数据后,将向客户端发送响应。
  4. 客户端将接收响应并打印出来。

程序流程图

image

Source: Socket Programming in C/C++ - GeeksforGeeks

服务端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>

#define IPADDR "127.0.0.1" /* 服务端 IP 地址 */
#define PORT 43866         /* 端口号 */
#define STRING_LEN_16 16
#define STRING_LEN_64 64
#define BACKLOG 10

char *_inet_ntoa(struct in_addr *addr, char *ipAddr, int len)
{
    // 将二进制 IP 地址转换为 可读 IP 地址
    if (NULL == addr || NULL == ipAddr || 16 > len)
    {
        printf("invalid param\n");
        return NULL;
    }
    unsigned char *tmp = (unsigned char *)addr;
    snprintf(ipAddr, len, "%d.%d.%d.%d", tmp[0], tmp[1], tmp[2], tmp[3]);
    return ipAddr;
}

int main(int argc, char *argv[])
{
    // 1. 创建套接字 socket()
    int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP 套接字
    if (sockfd == -1)
    {
        printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
        exit(0);
    }

    // 手动构建 sockaddr_in 结构体
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));    // 将结构体初始化为 0
    addr.sin_family = AF_INET;         // IPv4
    addr.sin_port = htons(PORT);       // 端口号
    inet_aton(IPADDR, &addr.sin_addr); // 将可读 IP 地址 转换为 二进制 IP 地址
    // inet_aton 现在已经不推荐使用,因为它不支持 IPv6

    // 2. 将套接字与特定的IP地址和端口绑定起来 bind()
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        printf("fail to call bind, errno[%d, %s]\n", errno, strerror(errno));
        close(sockfd);
        exit(0);
    }

    // 3. 让套接字进入被动监听状态 listen()
    if (listen(sockfd, BACKLOG) == -1)
    {
        printf("fail to call listen, errno[%d, %s]\n", errno, strerror(errno));
        close(sockfd);
        exit(0);
    }

    printf("server listen on %s:%u\n", IPADDR, PORT);

    // 4. 当套接字处于监听状态时,可以通过 accept 函数来接收客户端的请求 accept()
    struct sockaddr_in peerAddr;
    socklen_t peerAddrLen = sizeof(struct sockaddr_in);
    int connfd = accept(sockfd, (struct sockaddr *)&peerAddr, &peerAddrLen);
    if (connfd == -1)
    {
        printf("fail to call accept, errno[%d, %s]\n", errno, strerror(errno));
        close(sockfd);
        exit(0);
    }
    char peerIPAddr[STRING_LEN_16];
    _inet_ntoa(&peerAddr.sin_addr, peerIPAddr, STRING_LEN_16);
    printf("peer client address [%s:%u]\n", peerIPAddr, ntohs(peerAddr.sin_port));

    while (1)
    {
        // 5. 读取客户端发送的数据 recv()
        char buf[STRING_LEN_64];
        int n = recv(connfd, buf, STRING_LEN_64 - 1, 0);
        buf[n] = '\0';

        if (0 == n) // n为0表示对端关闭
        {
            printf("peer close\n");
            break;
        }

        printf("recv msg from client : %s\n", buf);

        sleep(2);

        // 6. 向客户端发送数据 send()
        char str[] = "recved!";
        printf("send msg to client : %s\n", str);
        send(connfd, str, strlen(str), 0);
    }

    // 7. 交互结束,关闭套接字 close()
    close(connfd);
    close(sockfd);

    return 0;
}

客户端示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>

#define IPADDR "127.0.0.1" /* 服务端 IP 地址 */
#define PORT 43866         /* 服务端 端口号 */
#define STRING_LEN_64 64
交互结束,关闭套接字 close()
int main()
{
    // 1. 创建TCP套接字 socket()
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("sockfd = %d\n", sockfd);
    if (-1 == sockfd)
    {
        printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
        exit(0);
    }

    // 2. 将套接字与特定的IP地址和端口号建立连接 connect()
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    inet_aton(IPADDR, &addr.sin_addr);
    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        printf("fail to call connect, errno[%d, %s]\n", errno, strerror(errno));
        close(sockfd);
        exit(0);
    }

    // 3. 通过套接字向服务端发送数据 send()
    char str[] = "hello world";
    printf("send msg to server : %s\n", str);
    send(sockfd, str, strlen(str), 0);

    // 4. 通过套接字从服务端接收数据 recv()
    char buf[STRING_LEN_64];
    int n = recv(sockfd, buf, STRING_LEN_64 - 1, 0);
    buf[n] = '\0';
    printf("recv msg from server : %s\n", buf);

    // 5. 交互结束,关闭套接字 close()
    close(sockfd);
}

编译运行

Compiling:
gcc client.c -o client
gcc server.c -o server

输出结果

$ ./server # run server on server terminal
server listen on 127.0.0.1:43866
peer client address [127.0.0.1:33844]
recv msg from client : hello world
send msg to client : recved!
peer close
$ ./client # run client on client terminal
sockfd = 3
send msg to server : hello world
recv msg from server : recved!

潜在问题

  1. 服务器程序只能同时服务一个客户端。
  2. 代码不支持IPv6。(inet_ntoa, inet_aton)
  3. 该程序没有任何安全措施。不会对客户端进行身份验证或加密通信,其容易受到欺骗或窃听等攻击。

解决方案

  1. 使用 select() 处理多个客户端。
  2. 使用 getaddrinfo(), ntop() 和 pton() 支持IPv6。
  3. 使用 SSL/TLS 加密通信。

参考资料

  1. Socket Programming in C/C++ - GeeksforGeeks

  2. Beej’s Guide to Network Programming

  3. Chapter 6 - Advanced UNIX Programming


最后修改于 2023-02-03


感谢您的支持 :D