java c语言 socket_网络通信 C语言 Socket TCP Select Server

前言

工作中遇到各种各样的网络通信。有MQTT和CoAP这样的物联网应用层协议,也有各种自定义的TCP或UDP协议。使用各种不同的计算机语言和框架开发网络通信,例如Java的Netty框架,C语言原始socket,Python Socket。各有各的使用场景,难易程度相差巨大。Netty上手困难,C语言编写复杂,Python Socket上手容易。

本文将介绍如何使用select套接字实现一个TCP服务器。我曾经翻阅了很多网络资料和图书示例,很难找到一个满意的select示例。

示例简述

主要通过select实现一个TCP Echo服务器

使用Python脚本实现一个TCP客户端,模拟多个客户端执行连接 -> 发送数据 -> 接收数据 -> 关闭连接

开发环境 Linux,构建工具CMake,编译器Linux GCC

tcp-select-server.c

代码实现

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#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;

}

代码说明

FD_SET(sfd, &read_fds) 把本地服务器套接字加入到读取列表中,一般这个sfd为3。

work_fds = read_fds 调用select方法之前务必要把read_fds 复制给一个临时变量work_fds,然后再通过select (max_sockfd + 1, &work_fds, NULL, NULL, &tout),注意select将改变work_fds和tout

cfd = accept(sfd, ..) 第一个连接的客户端的cfd一般为4,以此类推。

FD_SET(cfd, &read_fds); 接收到客户端连接后,务必把客户端的fd加入到read_fds

FD_CLR(fd, &read_fds);若recv获得长度为0,说明客户端已经断开连接,那么需要把客户端套接字从读取列表中移除

若不使用work_fds临时变量方法,也可以使用链表存储所有被接受的客户端fd。网上很多代码通过一个数组保存读取列表,这样容易产生溢出。例如【linux下socket采用select编程Demo】

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客户端线程,每个线程工作过程如下:

延时一个随机时间后,连接服务器

延时一个随机时间后,向服务器发送内容,并等待回复

延时一个随机时间后,关闭服务器连接

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)

结果分析

服务器依次使用了套接字编号 4,5,6,7,8。若出现第一个客户端套接字4已经断开,某客户端建立新连接时将会复用客户端4。也就是说空闲的客户端套接字将会被不断复用。

参考资料与代码仓库

物联网图书推荐 CoAP基础 徐凯《IoT开发实战: CoAP卷》 2017 机械工业出版社【 京东链接】

物联网图书推荐 CoAPs进阶 徐凯 崔红鹏《密码技术与物联网安全:mbedtls开发实战》2019 机械工业出版社 【京东链接】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值