Socket

Tecy 发布于 20 天前 74 次阅读


Linux Socket 是网络编程的核心接口,它提供了进程间通信(IPC)的能力,尤其用于不同主机(通过网络)或同一主机上的进程通信。它本质上是应用层与 TCP/IP 协议栈之间的编程接口(API),遵循“一切皆文件”的哲学,操作 Socket 就像操作文件一样(使用文件描述符)。

在 Linux 环境下使用 Socket。

#include <sys/socket.h>

socket()

创建 Socket:int socket(int domain, int type, int protocol);

domain

  • domain:参数用于指定协议族。PF_* (Protocol Family) 和 AF_* (Address Family) 宏定义在历史上有细微区别,但在现代 Linux 系统中实际上是完全等价且可互换的
用途推荐写法等价写法
IPv4 通信AF_INETPF_INET2
IPv6 通信AF_INET6PF_INET610
本地进程间通信AF_UNIXPF_UNIX1
原始网络包访问AF_PACKETPF_PACKET17
Netlink 通信AF_NETLINKPF_NETLINK16

type

  • type:指定套接字的通信类型和特性,它决定了数据传输的方式和套接字的行为。
类型说明
SOCK_STREAM流式套接字,TCP协议
SOCK_DGRAM数据报套接字,UDP协议
SOCK_RAW原始套接字,允许直接访问底层协议(如 IP、ICMP);可以自定义协议头部;需要 root 权限 (CAP_NET_RAW)
SOCK_SEQPACKET有序分组套接字,结合了流式和数据报的特点;提供可靠、有序的消息传输;保留消息边界

Linux 特有类型标志(与基本类型组合使用)。
SOCK_NONBLOCK :非阻塞模式,创建时直接设置为非阻塞模式;替代传统的 fcntl(fd, F_SETFL, O_NONBLOCK)

使用方法:

int nonblock_sock = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

protocol

protocol:用于指定要使用的具体网络协议。这个参数与 domain和 type一起决定了套接字的完整行为。

在大多数情况下,protocol 可以设置为 0,表示使用给定协议族和套接字类型的默认协议

  • domain = AF_INET + type = SOCK_STREAM → TCP (默认)
  • domain = AF_INET + type = SOCK_DGRAM → UDP (默认)
  • domain = AF_INET6 + type = SOCK_STREAM → TCPv6 (默认)
  • domain = AF_INET6 + type = SOCK_DGRAM → UDPv6 (默认)

特殊场景:当使用 SOCK_RAW 类型时,protocol 参数变得特别重要,它决定了:

  1. 套接字接收哪些类型的数据包。
  2. 套接字发送数据包时自动添加的协议头。

使用协议常量需要 #include <netinet/in.h>

协议常量用途说明
IPPROTO_ICMP发送/接收 ICMP 包 (ping, traceroute)
IPPROTO_IGMP组播管理协议
IPPROTO_IPIPIP-in-IP 隧道协议
IPPROTO_TCP直接处理 TCP 段 (需自行处理 TCP 头)
IPPROTO_UDP直接处理 UDP 数据报 (需自行处理 UDP 头)
IPPROTO_RAW发送时需要自行构造完整 IP 头
ETH_P_ALL接收所有以太网帧 (需使用 AF_PACKET domain)

组合结果:

+-------------+---------------+-------------------------+---------------------+
| domain      | type          | protocol                | result protocol     |
+-------------+---------------+-------------------------+---------------------+
| AF_INET     | SOCK_STREAM   | 0 or IPPROTO_TCP        | TCP                 |
| AF_INET     | SOCK_DGRAM    | 0 or IPPROTO_UDP        | UDP                 |
| AF_INET     | SOCK_RAW      | IPPROTO_ICMP            | ICMP                |
| AF_INET     | SOCK_RAW      | IPPROTO_RAW             | raw IP packet       |
| AF_INET6    | SOCK_STREAM   | 0 or IPPROTO_TCP        | TCP over IPv6       |
| AF_INET6    | SOCK_DGRAM    | 0 or IPPROTO_UDP        | UDP over IPv6       |
| AF_INET6    | SOCK_RAW      | IPPROTO_ICMPV6          | ICMPv6              |
| AF_PACKET   | SOCK_RAW      | ETH_P_ALL               | ethernet frame      |
| AF_NETLINK  | SOCK_RAW      | NETLINK_ROUTE           | route info          |
+-------------+---------------+-------------------------+---------------------+

return

返回值: 成功时返回新的 Socket 描述符(非负整数),失败返回 -1 并设置 errno
errno 需要 #include <errno.h>

错误码常量定义含义与典型原因
权限错误EACCES创建指定类型套接字权限不足(特别是原始套接字需要 root 权限)
地址族不支持EAFNOSUPPORT不支持的协议族(如在不支持 IPv6 的系统上使用 AF_INET6
无效参数EINVAL无效参数组合(如无效的类型/协议组合)
文件描述符超限EMFILE进程打开文件描述符数量已达上限(ulimit -n 查看限制)
系统文件表满ENFILE系统打开文件总数达到内核限制(/proc/sys/fs/file-max
无缓冲区空间ENOBUFS 或 ENOMEM内核内存不足,无法分配套接字资源
协议不支持EPROTONOSUPPORT系统不支持请求的协议或协议不可用
套接字类型不支持ESOCKTNOSUPPORT协议族不支持请求的套接字类型
协议不可用EPROTOTYPE套接字类型不支持请求的协议(如对 UDP 使用 SOCK_STREAM)
网络子系统未运行ENETDOWN网络子系统未运行(罕见,通常表示严重系统问题)

使用方法:

int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
    // errno 为全局变量(线程安全),需要保存以防被修改
    int errsv = errno;
    fprintf(stderr, "Error: %s\n", strerror(errsv)); 

    // ...
}

// ...

bind()

绑定 socket:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • 将创建的 Socket (sockfd) 与一个特定的本地地址 (addr) 和端口 (addrlen 是地址结构长度) 关联起来。
  • 服务器必须调用 bind 来指定监听的端口。
  • 客户端通常不需要显式调用 bind,内核会在 connect 或 sendto 时自动分配一个临时端口(称为临时端口 Ephemeral Port)。

sockfd

sockfd:套接字文件描述符,由 socket() 系统调用创建,需要有效且未绑定。

addr

addr:指向包含绑定地址信息的结构体指针。

通用结构:

struct sockaddr {
    sa_family_t sa_family;   // 地址族(AF_INET, AF_INET6 等)
    char        sa_data[14]; // 地址数据
};

ipv4:

struct sockaddr_in {
    sa_family_t    sin_family; // 地址族:AF_INET
    in_port_t      sin_port;   // 16位端口号(网络字节序)
    struct in_addr sin_addr;   // 32位 IPv4 地址
    unsigned char  sin_zero[8];// 填充字段(设为0)
};

struct in_addr {
    uint32_t s_addr; // IPv4 地址(网络字节序)
};

ipv6:

struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族:AF_INET6
    in_port_t       sin6_port;     // 16位端口号(网络字节序)
    uint32_t        sin6_flowinfo; // IPv6 流信息
    struct in6_addr sin6_addr;     // 128位 IPv6 地址
    uint32_t        sin6_scope_id; // 作用域ID
};

struct in6_addr {
    unsigned char s6_addr[16]; // IPv6 地址
};

unix:

struct sockaddr_un {
    sa_family_t sun_family;        // 地址族:AF_UNIX
    char        sun_path[108];     // 路径名
};

addrlen

addrlen:指定 addr 结构体的实际长度。

// IPv4
sizeof(struct sockaddr_in)

// IPv6
sizeof(struct sockaddr_in6)

// UNIX 域
sizeof(struct sockaddr_un)

return

返回值: 成功时返回 0,失败返回 -1 并设置 errno
errno 需要 #include <errno.h>

错误码常量原因与解决方案
权限错误EACCES绑定到特权端口(<1024)需要 root 权限
解决方案:使用 sudo 或绑定到 ≥1024 的端口
地址已使用EADDRINUSE请求的地址(IP+端口)已被占用
解决方案:使用 SO_REUSEADDR 选项或更换端口
地址不可用EADDRNOTAVAIL尝试绑定到不存在的网络接口
解决方案:使用 INADDR_ANY 或有效的本地 IP
错误文件描述符EBADFsockfd 不是有效的文件描述符
解决方案:检查套接字创建是否成功
无效参数EINVAL套接字已绑定/已连接,或地址族不匹配
解决方案:确保套接字未绑定,地址族正确
非套接字ENOTSOCKsockfd 不是套接字描述符
解决方案:检查文件描述符类型
内存不足ENOMEM内核内存不足
解决方案:释放系统资源或减少负载
只读文件系统EROFSUNIX 域套接字路径在只读文件系统
解决方案:更改绑定路径

使用示例:

sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_addr = INADDR_ANY, // 绑定到所有网络接口
    .sin_port = htons(8080) // 绑定到 8080 端口
};

// c++ 中
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;

if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
    int errsv = errno;
    fprintf(stderr, "Error: %s\n", strerror(errsv)); 
    // ...
}

// ...

listen()

监听:int listen(int sockfd, int backlog); 。将套接字从"主动"状态转为"被动"状态,准备接受客户端连接。

sockfd

要求

  • 必须是已创建并成功绑定的套接字。
  • 类型必须是 SOCK_STREAM 或 SOCK_SEQPACKET
  • 必须处于未连接状态。

backlog

指定等待连接队列的最大长度。队列满时连接将会被丢弃。

  • 内核队列机制
    • 未完成连接队列 (SYN队列)
      • 存储已收到客户端 SYN 但未完成三次握手的连接
      • 状态:SYN_RCVD
    • 已完成连接队列 (ACCEPT队列)
      • 存储已完成三次握手等待 accept() 的连接
      • 状态:ESTABLISHED
  • 现代系统行为
    • Linux 4.3+ 开始使用单个队列模型
    • backlog 参数仅控制已完成连接队列的大小
    • SYN 队列大小由 /proc/sys/net/ipv4/tcp_max_syn_backlog 控制

return

返回值: 成功时返回 0,失败返回 -1 并设置 errno
errno 需要 #include <errno.h>

错误码常量原因与解决方案
错误描述符EBADFsockfd 不是有效的文件描述符
非套接字ENOTSOCKsockfd 不是套接字描述符
已连接EISCONN套接字已处于连接状态
操作不支持EOPNOTSUPP套接字类型不支持监听(如 UDP)
地址已使用EADDRINUSE另一个套接字已在监听相同地址
无效参数EINVAL套接字未绑定或 backlog 为负

accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
用于从已完成连接队列中提取第一个连接请求,创建一个新的套接字,并返回其文件描述符。

  • 默认阻塞:若无连接请求,进程将阻塞等待。
  • 非阻塞模式(需用 fcntl() 设置):
    • 若无连接,立即返回 -1,并设 errno 为 EAGAIN 或 EWOULDBLOCK

sockfd

监听套接字描述符(由 socket() 创建并通过 listen() 进入监听状态)。类型必须是 SOCK_STREAM 或 SOCK_SEQPACKET

addr

指向存放客户端地址信息的缓冲区(如 struct sockaddr_in)。若不需要客户端地址,可设为 NULL

addrlen

  • 输入时:指向的值需设为 addr 缓冲区的长度(如 sizeof(struct sockaddr_in))。
  • 输出时:内核将其修改为实际地址结构的大小。
    若 addr 为 NULL,此参数也应为 NULL

return

返回值: 成功返回一个新的 sockfd(非负数),用于与客户端通信,失败返回 -1 并设置 errno
errno 需要 #include <errno.h>

错误码原因说明
EAGAIN/EWOULDBLOCK非阻塞模式下无连接可用
EBADFsockfd 无效
ECONNABORTED连接已中止(客户端在完成前关闭)
EINTR被信号中断(可安全重启调用)
EINVAL套接字未处于监听状态
EMFILE进程文件描述符耗尽
ENFILE系统文件描述符耗尽
EFAULTaddr 或 addrlen 指向无效地址
EPROTO协议错误(如客户端在连接建立前重置)

connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于客户端主动与服务器建立连接(TCP)或指定默认地址(UDP)。

  • 对于 TCP 套接字:触发三次握手过程,建立可靠连接。
  • 对于 UDP 套接字:不建立实际连接,但后续的 send()/write() 调用可省略目标地址(数据报默认发送至此地址)。
  • 默认阻塞:调用后进程阻塞,直到连接成功或失败(通常最长75秒)。
  • 非阻塞模式(需用 fcntl() 设置):
    • 立即返回 -1,并设 errno 为 EINPROGRESS(表示连接正在进行)。
    • 后续需用 select()/poll() 检查套接字是否可写(表示连接完成)。

sockfd

客户端套接字描述符(通过 socket() 创建)。

addr

指向服务器地址结构(如 struct sockaddr_in 或 struct sockaddr_in6)。必须包含服务器的 IP 地址和端口号。

addrlen

服务器地址结构的长度(如 sizeof(struct sockaddr_in))。

return

返回值: 成功返回 0, 失败返回 -1 并设置 errno
errno 需要 #include <errno.h>

错误码原因说明
EINPROGRESS非阻塞模式下连接正在建立(需后续检查)
EALREADY非阻塞套接字上已有连接尝试正在进行
ECONNREFUSED服务器拒绝连接(端口无监听)
ETIMEDOUT连接超时(服务器未响应)
ENETUNREACH网络不可达
EADDRINUSE本地地址已被占用
EINTR被信号中断(可安全重启调用)
EISCONN套接字已处于连接状态
EAFNOSUPPORT地址族不支持(如 IPv6 地址用于 IPv4 套接字)
EFAULTaddr 指向无效地址空间

recv()

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t read(int sockfd, void *buf, size_t count); = ssize_t recv(int sockfd, void *buf, size_t len, 0)

已连接的套接字接收数据(主要用于 TCP,也可用于已调用 connect() 的 UDP 套接字)。将接收到的数据存储到缓冲区 buf 中,最多接收 len 个字节。是 read() 的增强版,增加了额外的控制标志。

部分读(Partial Read):

  • 常见场景:
    • 接收缓冲区数据量小于请求的 count
    • 数据包分片到达。
    • 被信号中断。
  • 必须循环读取直到获取完整数据:
// 循环接收完整消息的范式
size_t total = 0;
while (total < expected_length) {
    ssize_t n = recv(sockfd, buf + total, len - total, 0);
    if (n <= 0) /* 处理错误或关闭 */;
    total += n;
}

数据边界:

  • TCP:面向字节流,无消息边界。可能返回任意长度(1~len),需循环读取完整消息。
  • UDP:面向数据报,每次调用接收一个完整数据报(若 buf 过小,多余数据被丢弃)。

阻塞行为:

  • 默认阻塞:无数据时进程挂起等待。
  • 非阻塞模式MSG_DONTWAIT 或 fcntl(O_NONBLOCK)):
    • 无数据时立即返回 -1errno=EAGAIN/EWOULDBLOCK

带外数据:

  • 使用 MSG_OOB 接收紧急数据(通常只占用 1 字节)。
  • 紧急指针指向最后一个带外字节。

sockfd

已经连接的套接字描述符。

buf

数据接收缓冲区的起始位置。

len

缓冲区的最大容量(字节数)。

flags

控制接收行为的标志位(常用值见下表,可通过 | 组合)。

标志值 (Linux)说明
MSG_PEEK0x02查看数据但不从接收队列移除(下次调用仍可读到相同数据)
MSG_OOB0x01接收带外数据(紧急数据)
MSG_WAITALL0x100阻塞直到请求的字节数全部接收完(除非遇到错误或连接关闭)
MSG_DONTWAIT0x40非阻塞操作(等价于 fcntl(O_NONBLOCK)
MSG_TRUNC0x20即使数据被截断也返回原始数据报长度(用于原始套接字)
MSG_ERRQUEUE0x2000从错误队列接收错误信息(用于诊断)

return

返回值:

  • 成功返回实际接收的字节数(>0)。
  • 连接关闭返回 0 。
  • 失败返回 -1,并设置 errno
错误码原因说明
EAGAIN/EWOULDBLOCK非阻塞模式下无可用数据
EBADF无效文件描述符
ECONNREFUSED远程主机拒绝连接(UDP)
EFAULTbuf 指向无效地址空间
EINTR被信号中断(可安全重启)
EINVAL无效参数(如负的 len 值)
ENOTCONN套接字未连接
ENOTSOCK文件描述符不是套接字
ETIMEDOUT连接超时
ECONNRESET连接被对方重置(如对方进程崩溃)

send()

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t write(int sockfd, const void *buf, size_t count); = ssize_t send(int sockfd, const void *buf, size_t len, 0);

已连接的套接字发送数据(TCP 或已连接 UDP),是 write() 的增强版,提供额外的控制标志,适用于需要特殊发送选项的场景(如带外数据、非阻塞操作等)。

阻塞行为:

  • 默认阻塞:当套接字发送缓冲区满时,进程将阻塞直到有空间。
  • 非阻塞模式(通过 fcntl(O_NONBLOCK) 设置):
    • 立即返回 -1errno=EAGAIN/EWOULDBLOCK(当缓冲区满时)。
    • 可能执行部分写(返回实际写入的字节数)。

部分发送:

  • 必须检查返回值并处理未发送数据:
size_t total_sent = 0;
while (total_sent < len) {
    ssize_t n = send(sockfd, buf + total_sent, len - total_sent, flags);
    if (n <= 0) /* 处理错误 */;
    total_sent += n;
}

sockfd

已连接的套接字描述符。

buf

指向待发送数据的缓冲区。

len

需要发送的字节数。

flags

控制发送行为的标志位(常用值如下,可通过 | 组合)。

标志值 (Linux)说明
MSG_OOB0x01发送带外数据(紧急数据)
MSG_DONTROUTE0x04绕过标准路由(仅限本地网络)
MSG_DONTWAIT0x40非阻塞操作(等价于 fcntl(O_NONBLOCK)
MSG_NOSIGNAL0x4000连接断开时不产生 SIGPIPE 信号
MSG_EOR0x80标记记录结束(SOCK_SEQPACKET 类型)
MSG_MORE0x8000提示内核后续还有数据(优化小包合并)
MSG_CONFIRM0x800确认链路有效(用于 ARP 表维护)

return

返回值: 成功返回实际发送的字节数,失败返回 -1 并设置 errno

错误码原因说明
EAGAIN/EWOULDBLOCK非阻塞模式下发送缓冲区满
EBADF无效文件描述符
ECONNRESET连接被对方重置(对方进程崩溃)
EPIPE连接已关闭(未设置 MSG_NOSIGNAL 时会触发 SIGPIPE)
EFAULTbuf 指向无效地址空间
EINTR被信号中断(可安全重启)
EINVAL无效参数(如负的 len 值)
ENOTCONN套接字未连接
ENOTSOCK文件描述符不是套接字
EMSGSIZE消息过大(UDP 数据报超过路径 MTU)
ENOBUFS系统缓冲区资源耗尽

sendto()

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

UPD 数据发送。

  • 必须指定目标地址 (dest_addr) 和地址长度 (addrlen)。
  • 如果 UDP Socket 已 connect,也可以使用 send 或 write(不需要指定目标地址)。

recvfrom()

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

UPD 数据接收。

  • 接收数据报,并通过 src_addr 和 addrlen 返回发送方的地址。
  • 如果 UDP Socket 已 connect,也可以使用 recv 或 read(不会返回发送方地址)。

sendmsg()

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

通用数据发送:最灵活,支持分散/聚集 I/O 和辅助数据。

recvmsg()

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

通用数据接收:最灵活,支持分散/聚集 I/O 和辅助数据。

close()

int close(int fd);

头文件 #include <unistd.h>

  • 关闭文件描述符(包括 Socket 描述符)。对于 TCP Socket,会触发四次挥手过程(如果必要)。
  • 如果有多个进程共享同一个 Socket 描述符(通过 fork),close 只是减少该描述符的引用计数。只有当引用计数为 0 时,才会真正关闭连接并释放资源。

shutdown()

int shutdown(int sockfd, int how);

  • 提供更精细的控制,关闭连接的一个方向或两个方向。
  • howSHUT_RD (关闭读端,不再接收数据), SHUT_WR (关闭写端,发送 FIN 通知对方不再发送数据), SHUT_RDWR (同时关闭读写)。
  • 不影响文件描述符的引用计数,无论有多少个进程持有该描述符,shutdown 都会立即影响连接的可用性。

利用 socket 实现简易C/S。
server.cpp

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

#include <iostream>
#include <thread>

void handle(int fd) {
    while (true) {
        const size_t N = 1024;
        char buf[N + 1] = {};
        size_t n = recv(fd, buf, N, 0);
        if (n > 0) {
            buf[n] = '\0';
            std::cout << "thread: " << std::this_thread::get_id() << " recv: " << buf << std::endl;
        } else {
            if (n == 0) {
                std::cout << "thread: " << std::this_thread::get_id() << " connection closed" << std::endl;
            } else {
                std::cerr << "thread: " << std::this_thread::get_id() << " recv error: " << strerror(errno) << std::endl; 
            }
            break;
        }
    }
}

void start_server(const char* ip, uint16_t port) {
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (fd < 0) {
        std::cerr << "create socket error: " << strerror(errno) << std::endl; 
        return;
    }

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
        std::cerr << "invalid ip: " << ip << std::endl;
        return;
    }

    if (bind(fd, (sockaddr*) &addr, sizeof(addr)) < 0) {
       std::cerr << "bind ip: " << ip << " error: " << strerror(errno) << std::endl;
       return; 
    }

    int backlog = 128;
    if (listen(fd, backlog) < 0) {
        std::cerr << "listen ip: " << ip << " error: " << strerror(errno) << std::endl; 
    }

    while (true) {
        sockaddr_in addr;
        socklen_t len = sizeof(addr);
        int req = accept(fd, (sockaddr*) &addr, &len);
        if (req < 0) {
            std::cerr << "accept error: " << strerror(errno) << std::endl;
        } else {
            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &addr.sin_addr, client_ip, sizeof(client_ip));
            std::cout << "accept " << client_ip << ":" << ntohs(addr.sin_port) << std::endl;

            // create thread.
            std::thread t(handle, req);
            t.detach();
        }
    }
}

int main() {
    start_server("127.0.0.1", 8080);
    return 0;
}

client.cpp

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

#include <iostream>

void start_client(const char*ip, uint16_t port) {
    int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (fd < 0) {
        std::cerr << "create socket error: " << strerror(errno) << std::endl; 
        return;
    }

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    if (inet_pton(AF_INET, ip, &addr.sin_addr) != 1) {
        std::cerr << "invalid ip: " << ip << std::endl;
        return;
    }

    if (connect(fd, (sockaddr*) &addr, sizeof(addr)) < 0) {
        std::cerr << "connect ip: " << ip << " error: " << strerror(errno) << std::endl;
        return;
    }

    while (true) {
        const size_t N = 1024;
        char buf[N];
        std::cin >> buf;
        send(fd, buf, strlen(buf), 0);
    }
}

int main() {
    start_client("127.0.0.1", 8080);
    return 0;
}