使用 Socket 建立多个 TCP 连接

笔记 · 10 天前 · 55 人浏览
使用 Socket 建立多个 TCP 连接

  使用Socket来建立多个TCP连接是一种常见的网络通信方式,在客户端和服务器之间进行数据交换。需要注意的是,服务器在处理多个客户端时,应该为每个客户端连接创建一个新的线程或进程,或者使用异步IO来避免阻塞。

  本文将以多线程和多进程两种方式实现,可以运行多个客户端实例来测试服务器是否能处理多个TCP连接。可以简单地在命令行中多次运行客户端脚本,或者写一个循环来多次创建和关闭客户端连接。

  在C语言中,实现一个基本的 TCP 服务器和客户端涉及使用 socket 编程接口,这通常包括 socket(), bind(), listen(), accept(), send(), recv()close() 等函数,之前有所介绍。

  Socket 编程是网络通信中的一项基础而强大的技术,它允许两台计算机(或同一台计算机上的两个进程)之间进行双向通信。Socket 可以看作是网络通信中的一个端点,通过它可以发送或接收数据。在 TCP/IP 协议族中, Socke...
2024-09-01
3
Socket 编程

示例程序

  以多线程多进程两种方式实现,支持多个客户端连接服务器。需要注意的是服务器将监听一个特定的端口,并接受来自客户端的连接。一旦连接建立,它将接收来自客户端的消息,并将其回显给客户端,而客户端的端口绑定时自动分配的,当然也可以修改程序成一个指定端口。

基于多线程支持多个 TCP 连接

server.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

#define PORT 6666
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
        if(result < 0)        \
        {                     \
            perror(cmd);      \
            return -1;        \
        }


void * read_from_client_then_write(void * arg)
{
    // 读取客户端发送过来的数据 回复收到
    int client_fd = *(int *)arg;
    char * read_buf = NULL;
    char * write_buf = NULL;

    ssize_t count = 0, send_count = 0;

    // 初始化
    read_buf = malloc(sizeof(char) * BUFFSIZE);
    write_buf = malloc(sizeof(char) * BUFFSIZE);

    if (!read_buf)
    {
        printf("服务端读缓存创建异常,断开连接\n");
        close(client_fd);
        perror("read_buf");
        return NULL;        
    }

    if (!write_buf)
    {
        printf("服务端写缓存创建异常,断开连接\n");
        close(client_fd);
        perror("read_buf");
        return NULL;        
    }

    while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
    {
        if (count < 0)
        {
            perror("recv");
        }
        // 接收数据打印到控制台
        printf("从_%d_客户端接收到数据:%s", client_fd, read_buf);
        // 把收到的消息写到写缓存
        strcpy(write_buf, "收到\n");
        send_count = send(client_fd, write_buf, BUFFSIZE, 0);
        if (send_count < 0)
        {
            perror("send");
        }
    }

    // 当客户端输入 ctrl+D 退出循环
    // shutdown(client_fd, SHUT_RD);
    close(client_fd);
    free(read_buf);
    free(write_buf);
}


int main(int argc, char const *argv[])
{
    struct sockaddr_in server_addr, client_addr;
    int sock_fd, tmp_res;

    // 清空
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写服务端类型
    server_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  0.0.0.0
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
    // 填写端口号
    server_addr.sin_port = htons(PORT);

    /******** 网络编程 socket 流程 ********/
    // 1. 创建socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    handle_error("socket", sock_fd);

    // 2. 绑定地址
    tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    handle_error("bind", tmp_res);

    // 3. 进入监听状态
    tmp_res = listen(sock_fd, 128);  // 等待队列长度,可以随便填
    handle_error("listen", tmp_res);

    // 4. 获取客户端的连接 : 需要能够接收多个连接
    socklen_t cliaddr_len = sizeof(client_addr);

    while (1)
    {
        int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len);
        // 不同的进程访问全局变量而导致的竞态资源的问题
        // 全局只创建了一个线程号实例,每次创建一个线程都会覆盖掉之前的线程号 
        pthread_t pid_read_write;
        handle_error("accept", client_fd);
        printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), 
                                            ntohs(client_addr.sin_port),
                                            client_fd);
        // 和每一个客户端使用一个线程交互  把客户端发送的信息打印到控制台 回复收到
        if (pthread_create(&pid_read_write, NULL, 
                read_from_client_then_write, (void *)&client_fd))
        {
            perror("pthread_create");
        }
        // 需要等待线程结束   但是不能挂起等待,否则就无法创建下一个连接
        pthread_detach(pid_read_write);
    }

    printf("释放资源\n");
    close(sock_fd);

    return 0;
}
client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

#define ADDRESS_S ("127.0.0.1")
#define ADDRESS_C ("192.168.100.137")
#define PORT_S 6666
#define PORT_C 8888
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
        if(result < 0)        \
        {                     \
            perror(cmd);      \
            return -1;        \
        }

void * read_from_server(void * arg)
{
    // 使用recv接收客户端发送的数据 打印到控制台
    char * read_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    read_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!read_buf)
    {
        perror("malloc_server_read_buf");
        return NULL;
    }

    // 接收数据
    // 只要能接受到数据 正常使用 一直挂起
    while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
    {
        if (count < 0)
        {
            perror("recv");
        }

        fputs(read_buf, stdout);
    }

    printf("服务端请求关闭\n");
    free(read_buf);

    return NULL;
}

void * write_to_server (void * arg)
{
    // 接收控制台输入的信息 写出去
    char * write_buf = NULL;
    int client_fd = *(int *)arg;  // 得到文件描述符

    write_buf = malloc(TYPESIZE);

    ssize_t count = 0;

    if (!write_buf)
    {
        perror("malloc_server_write_buf");
        return NULL;
    }

    while (fgets(write_buf, BUFFSIZE, stdin) != NULL)
    {
        // 发送数据
        count = send(client_fd, write_buf, BUFFSIZE, 0);
        if (count < 0)
        {
            perror("send");
        }
    }

    printf("接收到控制台的关闭请求, 不再写入... \n关闭连接\n");
    // 可以具体到关闭某一端
    shutdown(client_fd, SHUT_WR);
    free(write_buf);

    return NULL;
}

int main(int argc, char const *argv[])
{
    struct sockaddr_in server_addr, client_addr;
    int sock_fd, tmp_res;

    // 清空
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写服务端类型
    server_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  
    inet_pton(AF_INET, ADDRESS_S, &server_addr.sin_addr);
    // 填写端口号
    server_addr.sin_port = htons(PORT_S);

    /******** 网络编程 socket 流程 ********/
    // 1. 创建socket,只有一个文件描述符
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    handle_error("socket", sock_fd);

    // 2. 绑定 (自动分配)
    // 客户端可以不绑定,系统会自动分配一个空闲的端口号给客户端

    // 3. 客户端主动连接服务端
    tmp_res = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    handle_error("connect", tmp_res);

    printf("连接上服务端_%s:%d\n", 
        inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    // 创建子线程用于收消息
    pthread_t pid_read, pid_write;
    pthread_create(&pid_read, NULL, read_from_server, (void *)&sock_fd);
    // 创建子线程用于发消息
    pthread_create(&pid_write, NULL, write_to_server, (void *)&sock_fd);

    // 阻塞主线程
    pthread_join(pid_read, NULL);
    pthread_join(pid_write, NULL);

    // 释放资源
    printf("释放资源\n");
    close(sock_fd);

    return 0;
}
makefile
server:server.c
    - $(CC) -o $@ $^

client:client.c
    - $(CC) -o $@ $^
运行结果

多线程结果

基于多进程支持多个 TCP 连接

客户端程序和Makefile文件与上述一致,无需更改,唯一变动的是服务端程序,需要注意的是如何避免僵尸进程,新建进程后关闭不会使用到的资源。

server.c
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>


#define PORT 6666
#define BUFFSIZE 1024
#define TYPESIZE (sizeof(char) * BUFFSIZE)
#define handle_error(cmd, result) \
        if(result < 0)        \
        {                     \
            perror(cmd);      \
            return -1;        \
        }


void * read_from_client_then_write(void * arg)
{
    // 读取客户端发送过来的数据 回复收到
    int client_fd = *(int *)arg;
    char * read_buf = NULL;
    char * write_buf = NULL;

    ssize_t count = 0, send_count = 0;

    // 初始化
    read_buf = malloc(sizeof(char) * BUFFSIZE);
    write_buf = malloc(sizeof(char) * BUFFSIZE);

    if (!read_buf)
    {
        printf("服务端读缓存创建异常,断开连接\n");
        close(client_fd);
        perror("read_buf");
        return NULL;        
    }

    if (!write_buf)
    {
        printf("服务端写缓存创建异常,断开连接\n");
        close(client_fd);
        perror("read_buf");
        return NULL;        
    }

    while (count = recv(client_fd, read_buf, BUFFSIZE, 0))
    {
        if (count < 0)
        {
            perror("recv");
        }
        // 接收数据打印到控制台
        printf("从_%d_客户端接收到数据:%s", client_fd, read_buf);
        // 把收到的消息写到写缓存
        strcpy(write_buf, "收到\n");
        send_count = send(client_fd, write_buf, BUFFSIZE, 0);
        if (send_count < 0)
        {
            perror("send");
        }
    }

    // 当客户端输入 ctrl+D 退出循环
    // shutdown(client_fd, SHUT_RD);
    close(client_fd);
    free(read_buf);
    free(write_buf);
}

/**
 * 关闭回收所有的子进程,避免僵尸进程
 */
void zombie_dealer(int sig) {
    pid_t pid;
    int status;
    // 一个SIGCHLD可能对应多个子进程的退出
    // 使用while循环回收所有退出的子进程,避免僵尸进程的出现
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("子进程: %d 以 %d 状态正常退出,已被回收\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程: %d 被 %d 信号杀死,已被回收\n", pid, WTERMSIG(status));
        } else {
            printf("子进程: %d 因其它原因退出,已被回收\n", pid);
        }
    }
}


int main(int argc, char const *argv[])
{
    struct sockaddr_in server_addr, client_addr;
    int sock_fd, tmp_res;

    // 注册信号处理函数 回收子进程的资源 SIGCLD
    signal(SIGCLD, zombie_dealer);

    // 清空
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 填写服务端类型
    server_addr.sin_family = AF_INET;  // ipv4
    // 填写IP地址  0.0.0.0
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    // inet_pton(AF_INET, "0.0.0.0", &server_addr.sin_addr);
    // 填写端口号
    server_addr.sin_port = htons(PORT);

    /******** 网络编程 socket 流程 ********/
    // 1. 创建socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    handle_error("socket", sock_fd);

    // 2. 绑定地址
    tmp_res = bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    handle_error("bind", tmp_res);

    // 3. 进入监听状态
    tmp_res = listen(sock_fd, 128);  // 等待队列长度,可以随便填
    handle_error("listen", tmp_res);

    // 4. 获取客户端的连接 : 需要能够接收多个连接
    socklen_t cliaddr_len = sizeof(client_addr);

    while (1)
    {
        int client_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &cliaddr_len);
        // 不同的进程访问全局变量而导致的竞态资源的问题
        // 全局只创建了一个线程号实例,每次创建一个线程都会覆盖掉之前的线程号 
        pthread_t pid_read_write;
        handle_error("accept", client_fd);

        // 创建单独的子进程和连接的客户端交互
        pid_t pid = fork();
        if (pid < 0)
        {
            perror("fork");
        } else if (pid == 0)
        {
            // 子进程
            // 关闭不会使用的sock_fd
            close(sock_fd);
            printf("与客户端 %s:%d --> 建立连接_%d_\n", inet_ntoa(client_addr.sin_addr), 
                                            ntohs(client_addr.sin_port),
                                            client_fd);
            // 从客户端读取数据  并回复
            read_from_client_then_write((void *)&client_fd);
            // 关闭 client_fd
            close(client_fd);
            exit(EXIT_SUCCESS);
        } else
        {
            // 父进程
            // 关闭掉不使用的client_fd
            close(client_fd);
        }
    }

    printf("释放资源\n");
    close(sock_fd);

    return 0;
}
运行结果

多进程结果

  可以看到两个连接所创建的 client_fd 文件描述符都是4,是因为每次创建子进程后,父进程就会释放 client_fd,对应的文件描述符也就被释放,新的连接被创建时则会从可用的最小文件描述符开始占用,因此新的 client_fd 也是4。虽然 client_fd 是一样的,但对应的底层文件描述却是完全不同的。

C Socket
  1. xxcheng 9 天前

Theme Jasmine by Kent Liao

本网站由 又拍云 提供CDN加速/云存储服务

鄂ICP备2023005457号    鄂公网安备 42011302000815号