网络通信 C语言 Socket TCP Select Serve

2019-06-15  本文已影响0人  xukai871105

前言

工作中遇到各种各样的网络通信。有MQTT和CoAP这样的物联网应用层协议,也有各种自定义的TCP或UDP协议。使用各种不同的计算机语言和框架开发网络通信,例如Java的Netty框架,C语言原始socket,Python Socket。各有各的使用场景,难易程度相差巨大。Netty上手困难,C语言编写复杂,Python Socket上手容易。
本文将介绍如何使用select套接字实现一个TCP服务器。我曾经翻阅了很多网络资料和图书示例,很难找到一个满意的select示例。

示例简述

tcp-select-server.c

代码实现

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <assert.h>
#include <netdb.h>

#define BUF_SIZE    1024

/*
编译 gcc tcp-select-server.c -o tcp-select-server
运行 ./tcp-select-server 50018
*/
void print_sockaddr(struct sockaddr_in addr)
{
    // 保存点分十进制的地址
    char ip_address[INET_ADDRSTRLEN];
    int port;

    inet_ntop(AF_INET, &addr.sin_addr, ip_address, sizeof(ip_address));
    port = ntohs(addr.sin_port);
    printf("(%s:%d)\n", ip_address, port);
}

int main(int argc, char *argv[])
{
    int sfd = -1;
    struct addrinfo hints;
    struct addrinfo *result;
    struct addrinfo *rp;

    struct sockaddr_in client_addr;
    struct sockaddr_in peer_addr;
    fd_set read_fds;
    fd_set work_fds;

    struct timeval tout;
    int peer_addrlen;
    int client_addrlen;
    int optval;
    int max_sockfd;

    char recv_buf[BUF_SIZE];
    int port = 0;

    if (argc != 2) {
        fprintf(stderr, "usage: %s port\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;        /* 允许IPv4 或者 IPv6 */
    hints.ai_socktype = SOCK_STREAM;    /* TCP */
    hints.ai_flags = AI_PASSIVE;
    hints.ai_protocol = 0;
    hints.ai_canonname = NULL;
    hints.ai_addr = NULL;
    hints.ai_next = NULL;

    int s = getaddrinfo(NULL, argv[1], &hints, &result);
    if (s != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
        exit(EXIT_FAILURE);
    }
    for (rp = result; rp != NULL; rp = rp->ai_next) {
        sfd = socket(rp->ai_family, rp->ai_socktype,
                     rp->ai_protocol);
        if (sfd == -1)
            continue;

        if ((setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR,
                        &optval, sizeof (optval))) != 0)
            continue;

        if (bind(sfd, rp->ai_addr, rp->ai_addrlen) != 0)
            continue;

        if (listen (sfd, 5) != 0)
            continue;

        /* 成功 */
        break;
    }
    if (rp == NULL) {
        fprintf(stderr, "Could not bind\n");
        exit(EXIT_FAILURE);
    }
    freeaddrinfo(result);

    FD_ZERO(&read_fds);
    FD_SET(sfd, &read_fds);
    FD_SET(STDIN_FILENO, &read_fds);
    max_sockfd = sfd;

    while (1) {
        tout.tv_sec = 2;
        tout.tv_usec = 0;

        work_fds = read_fds;
        int ret = select (max_sockfd + 1, &work_fds, NULL, NULL, &tout);

        if (ret == 0) {
            continue;
        }
        if (ret == -1) {
            continue;
        }

        for (int i = 0; i < max_sockfd + 1; i++) {
            if (!FD_ISSET(i, &work_fds)) {
                continue;
            }

            int fd = i;
            if (fd == sfd) {
                client_addrlen = sizeof(client_addr);
                int cfd = accept(sfd, (struct sockaddr *)&client_addr,
                                 (socklen_t *)&client_addrlen);
                printf("accept fd:%d ", cfd);
                print_sockaddr(client_addr);

                if (cfd < 0) {
                    printf("accept cfd < 0!");
                    continue;
                }
                FD_SET(cfd, &read_fds);
                if (cfd > max_sockfd) {
                    max_sockfd = cfd;
                }
            } else {
                ssize_t num_read = recv(fd, recv_buf, sizeof(recv_buf), 0);
                if (num_read <= 0) {
                    printf ("client has left fd:%d\n", fd);
                    close(fd);
                    FD_CLR(fd, &read_fds);
                    continue;
                }

                recv_buf[num_read] = '\0';
                printf("receive %zd bytes: \"%s\" from fd:%d", num_read, recv_buf, fd);
                peer_addrlen = sizeof(peer_addr);
                getpeername(fd, (struct sockaddr *)&peer_addr, (socklen_t *)&peer_addrlen);
                print_sockaddr(peer_addr);

                send(fd, recv_buf, (size_t)num_read, 0);
            }
        }
    }

    return EXIT_SUCCESS;
}

代码说明

CMake构建

CMakeLists.txt内容如下:

cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 99)
add_executable(tcp-server tcp-server.c)

编译过程

mkdir -p build
cd build
cmake ..
make
# 最终生成可执行文件tcp-server

测试脚本

简要说明

编写一个python TCP客户端脚本,该脚本创建多个TCP客户端线程,每个线程工作过程如下:

  1. 延时一个随机时间后,连接服务器
  2. 延时一个随机时间后,向服务器发送内容,并等待回复
  3. 延时一个随机时间后,关闭服务器连接

tcp-multi-client.py

from concurrent.futures import ThreadPoolExecutor, wait
import time
import socket
import binascii
import random

HOST = '127.0.0.1'
PORT = 50018


def tcp_client():
    time.sleep(random.randint(1, 5))
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    print("connected remote", (HOST, PORT), "local", s.getsockname())
    request = bytearray([0x31, 0x32, 0x33, 0x34])

    time.sleep(random.randint(1, 5))
    s.sendall(request)
    response = s.recv(1024)
    print('received', binascii.hexlify(response), 'local', s.getsockname())

    time.sleep(random.randint(1, 5))
    print('closed local', s.getsockname())
    s.close()


# 创建一个线程池
executor = ThreadPoolExecutor(max_workers=5)
# 执行多次tcp_client
# executor.map(tcp_client, range(5))
all_task = [executor.submit(tcp_client) for _ in range(5)]
wait(all_task)

测试

【注意】代码多次调整,控制台输出内容与代码中打印细节不符。

先运行TCP服务器

# 可执行文件
cd build
./tcp-select-server 50018
# 运行python脚本后控制台输出
accept fd:4 (127.0.0.1:53704)
receive 4 bytes: "1234" from fd:4(127.0.0.1:53704)
accept fd:5 (127.0.0.1:53706)
accept fd:6 (127.0.0.1:53708)
accept fd:7 (127.0.0.1:53710)
accept fd:8 (127.0.0.1:53712)
receive 4 bytes: "1234" from fd:5(127.0.0.1:53706)
receive 4 bytes: "1234" from fd:8(127.0.0.1:53712)
client has left fd:4
receive 4 bytes: "1234" from fd:7(127.0.0.1:53710)
client has left fd:5
receive 4 bytes: "1234" from fd:6(127.0.0.1:53708)
client has left fd:8
client has left fd:7
client has left fd:6

再运行python脚本

python3 tcp-multi-client.py
# 控制台输出
connected remote ('127.0.0.1', 50018) local ('127.0.0.1', 53704)
received b'31323334' local ('127.0.0.1', 53704)
connected remote ('127.0.0.1', 50018) local ('127.0.0.1', 53706)
connected remote ('127.0.0.1', 50018) local ('127.0.0.1', 53708)
connected remote ('127.0.0.1', 50018) local ('127.0.0.1', 53710)
connected remote ('127.0.0.1', 50018) local ('127.0.0.1', 53712)
received b'31323334' local ('127.0.0.1', 53706)
received b'31323334' local ('127.0.0.1', 53712)
closed local ('127.0.0.1', 53704)
received b'31323334' local ('127.0.0.1', 53710)
closed local ('127.0.0.1', 53706)
received b'31323334' local ('127.0.0.1', 53708)
closed local ('127.0.0.1', 53712)
closed local ('127.0.0.1', 53710)
closed local ('127.0.0.1', 53708)

结果分析

参考资料与代码仓库

本文代码仓库 c-socket-demo

上一篇 下一篇

猜你喜欢

热点阅读