【网络-编程】一线程一连接 TCP 通信

声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。

摘要

对 Linux 系统中 TCP 协议相关套接字 API 进行简要介绍,基于阻塞 IO 模型,实现简单的服务器与客户端“一线程一连接”通信。


1 套接字概念

套接字(socket),是位于应用层和传输层之间的一个抽象层,它把复杂的 TCP/IP 操作抽象为几个简单的接口,供应用层调用,以实现不同进程在网络中的通信。

Socket 抽象层

2 套接字 API

套接字 API 定义了一组数据结构和操作。

一个 TCP 服务器端和客户端进行通信,调用的套接字 API,及基本步骤如下:

服务器端客户端说明
1. socket()1. socket()创建套接字(通信端点)
2. bind()服务器:将套接字绑定到指定 IP 和端口
3. listen()<<<2. connect()服务器:将套接字设置为监听模式(监听套接字),等待连接请求
客户端:向服务器发出连接请求
4. accept()服务器:接受连接请求(返回与当前客户端连接对应的“连接套接字”)
5. recv ()<<<3. send()客户端:发送数据
服务器:接收数据(使用连接套接字)
6. send()>>>4. recv()客户端:接收数据
服务器:发送数据(使用连接套接字)
7. close()5. close()客户端:关闭套接字,结束通信
服务器:关闭连接套接字,断开与客户端的连接;关闭监听套接字,结束所有服务

通过 man7.org/linuxlinux.die.net 可以查看套接字函数的详细说明。

2.0 头文件

使用套接字函数时,通常需要包含以下几个头文件:

#include <sys/socket.h>  // 包含 socket 核心函数和数据结构的声明
#include <netinet/in.h>  // 包含 IPv4 和 IPv6 地址族相关声明
#include <arpa/inet.h>   // 包含 IP 地址相关的一些函数
#include <unistd.h>      // 包含 close 函数
#include <errno.h>       // 包含 errno 定义

2.1 socket() 函数

/**
 * @file  <sys/socket.h>
 * @brief  创建一个通信端点(套接字),返回一个指向该端点的文件描述符(当前进程中未打开的最低编号)
 *         如果进程内没有打开其它文件,描述符值为 3(值 0~2 分别对应 stdin、stdout、stderr)。
 *         需要使用 close() 关闭文件。
 *
 * @param[in] domain 指定通信所使用的协议族(地址族 address family,AF_*),
 *                   值为 AF_INET,对应 IPv4 协议族;
 *                   值为 AF_INET6,对应 IPv6 协议族;
 * @param[in] type 指定套接字的类型(SOCK_*),
 *                 值为 SOCK_STREAM,对应字节流套接字类型;
 *                 值为 SOCK_DGRAM,对应数据报套接字类型
 * @param[in] protocol 指定通信所使用的协议,通常和前面两个参数都有关
 *                     值为 0,表示使用默认协议(在给定的协议族中,只存在一个支持特定套接字类型的协议)
 *                     如果协议族是 AF_INET,套接字类型是 SOCK_STREAM,默认协议是 TCP(IPPROTO_TCP)
 *                     如果协议族是 AF_INET,套接字类型是 SOCK_DGRAM,默认协议是 UDP(IPPROTO_UDP)
 * @return  如果成功,返回一个 int 类型的文件描述符,可以用来指向新创建的套接字;
 *          如果失败,返回 ‒1,可以通过 errno 获取错误信息。
 */
int socket(int domain, int type, int protocol);
// 创建“IPv4 协议族、字节流类型、TCP 协议”的套接字(通信端点)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

2.2 bind() 函数

/**
 * @file  <sys/socket.h>
 * @brief  将 addr 指定的地址分配给由文件描述符 sockfd 指向的套接字。
 *         既可以用于连接的(流式)套接字,也可以用于无连接的(数据报)套接字。
 *         创建一个 socket 后,套接字数据结构中有一个默认的 IP 地址和端口号,
 *         服务程序必须调用 bind 函数来绑定自己的 IP 地址和特定的端口号;
 *         客户程序通常不调用 bind,使用默认的 IP 和端口号与服务器程序通信。
 *
 * @param[in] sockfd 待绑定的套接字的描述符
 * @param[in] addr 指向结构体 sockaddr 的指针,其定义如下(主要用于类型转换)
 *                 struct sockaddr {
 *                     sa_family_t sa_family;  // Address family
 *                     char        sa_data[];  // Socket address
 *                 }
 *                 实际传入的结构体依赖于地址族的类型,如 AF_INET 对应 sockaddr_in
 * @param[in] addrlen 表示 addr 指向结构体的长度(以字节为单位)
 * @return  成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
struct sockaddr_in addr;         // 地址族 AF_INET 对应的地址结构为 sockaddr_in
memset(&addr, 0, sizeof(addr));  // 初始化
addr.sin_family = AF_INET;       // 与 socket 保持一致

addr.sin_addr.s_addr = htonl(INADDR_ANY);               // 任意网卡 IP 地址
// addr.sin_addr.s_addr = inet_addr("127.0.0.1");       // 回环地址,用于本机进程通信
// addr.sin_addr.s_addr = inet_addr("192.168.126.128"); // 可以 ping 通就可以通信

addr.sin_port = htons(2048);   // 设置端口,htons 将主机字节序转网络字节序(short 类型)
                               // 1024 以下的端口号为特权端口,只有特权进程可以绑定

// 将 addr 强制转换为 bind 函数形参的类型 struct sockaddr *
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));

// 获取本地套接字地址
struct sockaddr_in server_addr;
socklen_t addrlen = sizeof(server_addr);
getsockname(sockfd, (struct sockaddr *)&server_addr, &addrlen);
printf("Server Addr [%s:%d]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

2.2.1 sockaddr_in,IPv4 协议族套接字地址:

/**
 * @file  <netinet/in.h>
 * @brief 描述 IPv4 协议族套接字地址。
 *        sin_port 和 sin_addr 成员按网络字节顺序存储
 * 特殊的 IP 地址:
 *        INADDR_LOOPBACK (127.0.0.1),通过回环设备指向本地主机
 *        INADDR_ANY (0.0.0.0),可以绑定任意网卡 IP 地址
 */
struct sockaddr_in {
    sa_family_t    sin_family;  // 地址族:AF_INET
    in_port_t      sin_port;    // 端口(网络字节序)
    struct in_addr sin_addr;    // IPv4 地址
};
struct in_addr {
    in_addr_t s_addr;           // 地址(网络字节序)
};
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;

2.2.2 htonl/htons,主机字节序和网络字节序转换:

/**
 * @file  <arpa/inet.h>、<netinet/in.h>
 * @brief 主机字节序和网络字节序转换。
 *        小端字节序(Little Endian),数据的低字节存于内存低地址,高字节存于内存高地址
 *        大端字节序(Big Endian),数据的高字节存于内存低地址,低字节存于内存高地址
 *        主机字节序有的为小端(X86,很多的 ARM、DSP),有的为大端(KEIL C51)
 *        网络字节序为大端    
 */
uint32_t htonl(uint32_t hostlong);   // 主机字节序转网络字节序
uint16_t htons(uint16_t hostshort);  // from host byte order to network byte order
uint32_t ntohl(uint32_t netlong);    // 网络字节序转主机字节序
uint16_t ntohs(uint16_t netshort);   // from network byte order to host byte order

2.2.3 inet_addr,IP 地址格式转换:

/**
 * @file  <arpa/inet.h>
 * @brief 将点分十进制形式的字符串 IP 地址与二进制 IP 地址(网络字节序)相互转换
 */
in_addr_t inet_addr(const char *cp);  // 将点分十进制字符串 IP 地址转换为二进制地址
char *inet_ntoa(struct in_addr in);   // 将结构体 in_addr 类型的 IP 地址转换为点分十进制
                                      // 返回的字符串位于静态分配的缓冲区中,内容会被后续的调用覆盖
// 如果 af 是 AF_INET,src 为 in_addr_t*,dst 为用于保存地址字符串的缓冲区,size 为缓冲区大小
const char *inet_ntop(int af, const void *restrict src, char *restrict dst, socklen_t size);

2.2.4 getsockname,获取套接字地址:

/**
 * @file  <sys/socket.h>
 * @brief  在 bind 或 connect 后,获取本地套接字地址
 *
 * @param[in] sockfd 本地套接字描述符 
 * @param[out] addr 套接字地址结构体指针,用于保存本地套接字地址信息
 * @param[inout] addrlen 输入为 addr 所指结构体的大小(以字节为单位)
 *                       输出为 addr 中实际保持地址的长度(地址超长会被截断)
 * @return  成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
/**
 * @brief  获取通信对端的套接字地址
 * @param[in] sockfd 对端套接字描述符 
 */
int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);

2.3 listen 函数

/**
 * @file  <sys/socket.h>
 * @brief  在服务器端,使流套接字处于监听状态,监听客户端发来的建立连接请求。
 *         处于监听状态的流套接字将维护一个客户连接请求队列。
 *
 * @param[in] sockfd 已绑定的流套接字描述符 
 * @param[in] backlog 连接请求队列所能容纳的最大客户连接请求数量
 *                    达到最大连接请求数量后,新的客户端连接请求会失败(ECONNREFUSED)
 * @return  成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int listen(int sockfd, int backlog);
// 设置最大客户连接请求数量为 100
listen(sockfd, 100);

2.4 accept 函数

/**
 * @file  <sys/socket.h>
 * @brief  服务程序从“处于监听状态的流套接字”的客户端连接请求队列中,
 *         取出排在最前的一个客户端请求,
 *         并且创建一个新的套接字,来与客户端套接字建立连接通道。
 *         同样需要使用 close() 关闭新建的套接字。
 *         
 * @param[in] sockfd 处于监听状态的流套接字的描述符
 * @param[out] addr 为 null 或 套接字地址结构体指针,用于保存客户端地址信息
 * @param[inout] addrlen 输入为 null 或 addr 指向结构体的长度(以字节为单位)
 *                       输出为 addr 中实际保持地址的长度(地址超长会被截断)
 * @return  如果成功,返回新套接字的描述符,该套接字将与客户端套接字进行通信;
 *          如果失败,返回 ‒1,可以通过 errno 获取错误信息,此时 addrlen 内容不变。
 */
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
struct sockaddr_in client_addr;
socklen_t client_addrlen = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen);
int client_fd = accept(sockfd, NULL, NULL);

2.5 connect 函数

/**
 * @file  <sys/socket.h>
 * @brief  客户端套接字请求与服务器端的监听套接字建立连接(SOCK_STREAM)。
 *         如果套接字还没有被绑定,会把它绑定到一个未使用的本地地址。
 *         对于阻塞套接字,如果连接不上通常要等较长时间才能返回;
 *         对于非阻塞套接字,如果返回 ‒1,并且 errno 为 EINPROGRESS,
 *         那么后续可以用 select 函数检查这个连接是否建立成功。
 *
 * @param[in] sockfd 客户端还未连接的套接字描述符
 * @param[in] addr 对于 SOCK_STREAM,是服务器端套接字的地址信息
 *                 对于 SOCK_DGRAM,是用于收发数据的目标地址(并不建立连接)
 * @param[in] addrlen 表示 addr 指向结构体的长度(以字节为单位)
 * @return  成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
#define SERVER_IP "192.168.126.128"
#define SERVER_PORT 2048

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

2.6 send 函数

/**
 * @file  <sys/socket.h>
 * @brief  在已建立连接的 socket 上发送数据。
 *         只是把参数 buf 中的数据发送(拷贝)到套接字的发送缓冲区中(TCP 协议),
 *         此时数据并不一定马上被传送到连接的另一端,发送数据到接收端是底层协议完成的。
 *         如果协议后续发送数据到接收端出现网络错误,那么下一个 socket 函数就会返回 ‒1,
 *         因为其它 socket 函数,在执行的最开始总要先等待套接字的发送缓冲区中的数据被协议传送完毕。
 *
 * @param[in] sockfd 发送端套接字的描述符
 * @param[in] buf 存放待发送数据的缓冲区
 * @param[in] len 在 buf 缓冲区中,待发送数据的长度(以字节为单位)
 * @param[in] flags 消息传输的类型,一般设为 0
 * @return  如果拷贝数据成功,返回实际拷贝的字节数;
 *          如果在拷贝数据时出现错误则返回 ‒1,可以通过 errno 获取错误信息。
 */
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
char buf[] = "hello";
int wlen = send(sockfd, buf, strlen(buf), 0);
if (wlen == -1)               // 发生错误
    //...                     // 非阻塞模式下,如果 errno 为 EAGAIN/EWOULDBLOCK、EINTR 需重试
else if (wlen != strlen(buf)) // 成功发送部分数据
    //...                     // 继续发送剩余数据

发送函数对比:

  • send 只能用于基于连接的套接字;
  • sendtosendmsg 既能用于无连接的套接字,也能用于基于连接的套接字。

2.7 recv 函数

/**
 * @file  <sys/socket.h>
 * @brief  从“连接的套接字”或“无连接的套接字”上接收数据
 *         只是将内核接收缓冲区中的数据拷贝到应用层用户缓存区 buf 中,
 *         真正接收数据是由底层协议完成的。
 *
 * @param[in] sockfd 已连接或已绑定(针对无连接)的套接字的描述符
 * @param[in] buf 指向一个缓冲区,用来存放从套接字的接收缓冲区中拷贝的数据
 * @param[in] len 表示 buf 所指缓冲区的大小(以字节为单位)
 *  如果待接收数据长度 > len
 *    对于基于流的套接字(如 SOCK_STREAM),需要多次调用 recv 函数;
 *    对于基于消息的套接字(如 SOCK_DGRAM),多出的字节会被丢弃。   
 * @param[in] flags 消息接收的类型,一般设为 0
 *  值 MSG_PEEK,查看传入的消息(函数调用后,数据仍被视为未读)
 *  值 MSG_WAITALL,
 *    对于 SOCK_STREAM 套接字,这个标志请求函数阻塞,直到可以返回完整的数据;
 *    对于 SOCK_DGRAM 套接字,以及指定了 MSG_PEEK 等一些其他情况,函数可能会返回较少的数据。
 * @return  如果接收成功,返回收到数据的字节数(>0);
 *          如果连接被关闭(对方调用了 close),则返回 0;
 *          如果发生错误,则返回 ‒1,可以通过 errno 获取错误信息。
 */
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
char buf[128];
int rlen = recv(sockfd, buf, sizeof(buf), 0);
if (rlen == -1)       // 发生错误
    //...             // 非阻塞模式下,如果 errno 为 EAGAIN/EWOULDBLOCK、EINTR 需重试
else if (rlen == 0)   // 对端调用 close 关闭了连接
    //...             // 关闭本端连接
else                  // 成功接收
    //...

接收函数对比:

  • recv 一般用于基于连接的套接字;
  • recvfromrecvmsg 既能用于无连接的套接字,也能用于基于连接的套接字。

2.8 close 函数

/**
 * @file  <unistd.h>
 * @brief  关闭一个套接字
 *
 * @param[in] fd 要关闭的套接字的描述符
 * @return  成功返回 0;失败返回 ‒1,可以通过 errno 获取错误信息。
 */
int close(int fd);
close(sockfd);

3 示例代码

3.1 服务器端(1对1通信)

基于阻塞套接字,通过单线程,服务器依次与每个客户端进行通信。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SOCK_IP INADDR_ANY
#define SOCK_PORT 2048
#define LISTEN_BACKLOG 10
#define BUF_SIZE 256

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main()
{
    int sockfd;
    socklen_t addrlen;
    struct sockaddr_in addr, server_addr;

    // 创建一个套接字,用于监听客户端的连接
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
        handle_error("socket");

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(SOCK_IP);
    addr.sin_port = htons(SOCK_PORT);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
        handle_error("bind");

    // 获取本地套接字地址
    addrlen = sizeof(server_addr);
    if (getsockname(sockfd, (struct sockaddr *)&server_addr, &addrlen) == -1)
        handle_error("getsockname");
    printf("Server Addr [%s:%d]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    if (listen(sockfd, LISTEN_BACKLOG) == -1)
        handle_error("listen");

    while (1) // 在循环中一次服务一个客户端
    {
        int clientfd;
        struct sockaddr_in client_addr;
        printf("-----waiting for a client-----\n");

        // 从连接请求队列中取出排在最前面的客户端请求,如果队列为空则阻塞
        addrlen = sizeof(client_addr);
        clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
        if (clientfd == -1)
            handle_error("accept");
        printf("accept [%s:%d]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 发送服务说明
        char buf[BUF_SIZE];
        sprintf(buf, "Welcome [%s:%d] to the server!\n"
                     "Send \"bye\" to end the communication,\n"
                     "Send \"q\" to quit the service,\n"
                     "Send anything else to continue the communication.\n",
                inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        int wlen = send(clientfd, buf, strlen(buf), 0);
        if (wlen != strlen(buf))
            handle_error("send");

        while (1) // 在循环中与当前客户端通信
        {
            // 接收数据
            int rlen = recv(clientfd, buf, sizeof(buf), 0);
            if (rlen == -1)
                handle_error("recv");
            if (rlen == 0)
                break;
            buf[rlen] = '\0';
            printf("recv: %s(%d)\n", buf, rlen);

            // 通信控制
            if (strcmp(buf, "bye") == 0) {
                break;
            }
            else if (strcmp(buf, "q") == 0) {
                if (close(clientfd) == -1)
                    handle_error("close clientfd");
                if (close(sockfd) == -1)
                    handle_error("close sockfd");
                return 0;
            }

            // 发送数据
            wlen = send(clientfd, buf, rlen, 0);
            if (wlen != rlen)
                handle_error("send");
            printf("send: %s(%d)\n", buf, wlen);
        }

        if (close(clientfd) == -1)
            handle_error("close clientfd");
    }

    return 0;
}

3.2 服务器端(1对多通信)

基于阻塞套接字,通过多线程,服务器同时与多个客户端进行通信。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SOCK_IP INADDR_ANY
#define SOCK_PORT 2048
#define LISTEN_BACKLOG 10
#define BUF_SIZE 256

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
#define handle_error_en(en, msg) \
    do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

void *client_proc(void *arg);

int main()
{
    int sockfd;
    socklen_t addrlen;
    struct sockaddr_in addr, server_addr;

    // 创建一个套接字,用于监听客户端的连接
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
        handle_error("socket");

    // 允许地址的立即重用
    // int setsockopt(int sockfd, int level, int option_name,
    //                const void *option_value, socklen_t option_len);
    int on = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
        handle_error("setsockopt");

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(SOCK_IP);
    addr.sin_port = htons(SOCK_PORT);

    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
        handle_error("bind");

    // 获取本地套接字地址
    addrlen = sizeof(server_addr);
    if (getsockname(sockfd, (struct sockaddr *)&server_addr, &addrlen) == -1)
        handle_error("getsockname");
    printf("Server Addr [%s:%d]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

    if (listen(sockfd, LISTEN_BACKLOG) == -1)
        handle_error("listen");

    while (1) // 在循环中等待客户端连接
    {
        // 从连接请求队列中取出排在最前面的客户端请求,如果队列为空则阻塞
        int clientfd = accept(sockfd, NULL, NULL);
        if (clientfd == -1)
            handle_error("accept");

        pthread_t thread_id;
        int ret = pthread_create(&thread_id, NULL, client_proc, (void *)(long)clientfd);
        if (ret != 0)
            handle_error_en(ret, "pthread_create");
        pthread_detach(thread_id);
    }

    // 未实现多线程退出
    if (close(sockfd) == -1)
        handle_error("close sockfd");

    return 0;
}

void *client_proc(void *arg)
{
    int clientfd = (int)(long)arg;

    // 获取客户端 IP 和端口
    char ip[INET_ADDRSTRLEN]; 
    struct sockaddr_in client_addr;
    socklen_t addrlen = sizeof(client_addr);
    if (getpeername(clientfd, (struct sockaddr *)&client_addr, &addrlen) == -1)
        handle_error("getpeername");
    
    uint16_t port = ntohs(client_addr.sin_port);
    if (inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip)) == NULL)
        handle_error("inet_ntop");
    printf("accept [%s:%d]\n", ip, port);

    // 发送服务说明
    char buf[BUF_SIZE];
    sprintf(buf, "Welcome [%s:%d] to the server!\n"
                 "Send \"bye\" to end the communication,\n"
                 "Send anything else to continue the communication.\n",
            ip, port);

    int wlen = send(clientfd, buf, strlen(buf), 0);
    if (wlen != strlen(buf))
        handle_error("send");

    while (1) // 在循环中与客户端通信
    {
        // 接收数据
        int rlen = recv(clientfd, buf, sizeof(buf), 0);
        if (rlen == -1)
            handle_error("recv");
        if (rlen == 0)
            break;
        buf[rlen] = '\0';
        printf("[%s:%d] recv: %s(%d)\n", ip, port, buf, rlen);

        // 通信控制
        if (strcmp(buf, "bye") == 0) {
            break;
        }

        // 发送数据
        wlen = send(clientfd, buf, rlen, 0);
        if (wlen != rlen)
            handle_error("send");
        printf("[%s:%d] send: %s(%d)\n", ip, port, buf, wlen);
    }

    if (close(clientfd) == -1)
        handle_error("close clientfd");
}

3.3 客户端

基于阻塞套接字的 TCP 客户端示例。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "192.168.126.128"
#define SERVER_PORT 2048
#define BUF_SIZE 256

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1)
        handle_error("socket");

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
        handle_error("connect");

    char buf[BUF_SIZE];
    while (1) // 在循环中与服务器端通信
    {
        // 接收数据
        int rlen = recv(sockfd, buf, sizeof(buf), 0);
        if (rlen == -1)
            handle_error("recv");
        if (rlen == 0)
            break;
        buf[rlen] = '\0';
        printf("recv: %s(%d)\n", buf, rlen);

        // 从 stdin 读取一行数据
        fgets(buf, sizeof(buf), stdin);
        int datalen = strlen(buf);
        if (datalen != 1)
            buf[--datalen] = '\0'; // 如果非空,去掉换行符

        // 发送数据
        int wlen = send(sockfd, buf, datalen, 0);
        if (wlen != datalen)
            handle_error("send");
    }

    if (close(sockfd) == -1)
        handle_error("close");

    return 0;
}

参考

  1. 朱文伟,李建英著.Linux C/C++ 服务器开发实践.清华大学出版社.2022.

宁静以致远,感谢 King 老师。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值