Quanker3 / TCP Socket 网络编程实例

Created Tue, 09 Apr 2024 20:53:51 +0800 Modified Sun, 14 Apr 2024 10:05:13 +0800
7313 Words

linux: Socket 网络框架与编程示例

Socket 网络编程框架

Socket(套接字)是一个网络编程概念,描述了一个通信端点(Endpoint),用于建立网络连接(Connection)并传输数据。

Linux Kernel 提供了一套面向 Socket 的网络编程框架,并通过提供一组标准的 System call APIs,使得开发者可以在 Userspace 中便捷的开发各种 Network Applications,例如:基于 HTTP 协议的 Web 服务器、基于 SMTP 协议的邮件服务器、基于 FTP 协议的文件服务器等等。

Linux Socket 网络编程框架主要由 3 大模块组成:

  1. BSD Socket APIs
  2. Socket Abstraction Layer
  3. VFS Layer

img1

BSD Socket APIs 概览

BSD Socket APIs(Berkeley Software Distribution Socket APIs),是面向 Userspace Application 的接口封装层,提供了一套兼容绝大部分网络通信协议族的标准 Socket APIs。

  • socket():创建一个新的 socket,返回一个 int 类型的 socket fd(File Descriptor,套接字文件描述符),用于后续的网络连接操作。
  • bind():将 socket 与一个本地 IP:Port 绑定,通常用于服务端,以便在本地监听网络连接。
  • connect():建立与远程主机的连接,通常用于客户端,以便连接到远程服务器。
  • listen():开始监听来自远程主机的连接请求,通常用于服务器端,等待来自客户端的连接请求。
  • accept():接受一个连接请求,返回一个新的 socket fd,通常用于服务器端,用于接收客户端的连接请求。
  • send():向 socket 发送数据。
  • recv():从 socket 接收数据。
  • close():关闭 socket 连接。

Socket API 的使用通常可以分为以下几个步骤:

  1. 创建套接字:使用 socket() 函数创建一个新的 socket fd。
  2. 配置套接字:使用一些其他的 Socket API 函数,例如 bind()、connect() 和 listen() 来配置 socket,使其能够接收和发送数据。
  3. 数据传输:使用 send() 和 recv() 函数进行数据传输。
  4. 关闭套接字:使用 close() 函数关闭 socket 连接。

需要注意的是,Socket API 并不是线程安全的,如果有多个线程同时使用了同一个 socket fd,则可能会导致数据传输错误或其他问题。为了避免这种情况,Application 需要进行适当的同步数据处理


Socket Abstraction Layer

Socket Abstraction Layer(Socket 抽象层),是 Socket API 的底层支撑,主要负责以下工作:

  1. 实现了 Socket File System(套接字文件系统),用于管理 User Process 和 socket fd 之间的关系,包括 socket fd 的创建、打开、读写等操作。

  2. 实现了 Struct Socket、Struct Sock、Protocol Family(协议族)、Address Family(地址族)等数据结构

  3. 实现了 TCP/IP 协议栈,包括:TCP、UDP、ICMP 等协议。

  4. 实现了 L4 传输层功能,处理传输层协议的连接建立、数据传输、连接维护等操作。


Socket & Sock

socket_struct

  • Struct Socket 是在 Socket Layer 中定义的数据结构,面向上层 Socket API,包含了一个 Socket 所具有的各种属性,例如:状态、类型、标记、关联的 Sock 等。
  • Struct Sock 是在 Sock Layer 中定义的数据结构,面向底层协议栈实现,表示一个 Socket 对应的 PCB(Protocol Control Block,协议控制块),即:与某种网络协议相关的一些信息和状态,例如:TCP PCB 就包括了 TCP 连接状态发送缓冲区接收缓冲区拥塞窗口等。

Socket LayerNetwork Driver(网络设备驱动程序)之间通过 Socket Buffer(skb_buff)进行交互,当 Socket Layer 接收到 Application 的数据时,会将数据存储在 Socket Buffer 中,并将 Socket Buffer 传递给对应的 Sock Layer 进行处理。Struct Socket 和 Struct Sock 之间通过指针进行关联绑定,共同实现 Socket API 的功能。

img2

Socket Layer
/ linux/include/linux/net.h

/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
    
 struct socket {
 	socket_state      state;
 	short           type;   // 套接字类型,如 SOCK_STREAM、SOCK_DGRAM 等;
 	unsigned long      flags;  // 套接字标志,如 O_NONBLOCK、O_ASYNC 等;
 	struct file          *file;  // 套接字对应的文件结构体;
 	struct sock          *sk;    // 指向套接字对应的 Sock 结构体;
 	const struct proto_ops *ops;   // 套接字对应的操作函数集,如 inet_stream_ops、inet_dgram_ops 等;
 	struct socket_wq     wq;     // 套接字等待队列;
};  

typedef enum
{
  SS_FREE=0;      // 未分配
  SS_UNCONNECTED;  // 未连接到任何套接字
  SS_CONNECTING;  // 处于连接过程中
  SS_CONNECTED;   // 已经连接到另一个套接字
  SS_DISCONNECTING;     // 处于断开连接过程中
} socket_state;
Sock Layer

Struct Sock 包含了 Socket 的各种底层执行状态和操作信息,例如:接收和发送缓冲区、套接字队列、套接字协议信息等。

// linux/include/net/sock.h

struct sock {
    /* Socket family and type */
    unsigned short      family;  // 协议族,如 AF_INET、AF_PACKET 等;
    __u16               type;    // 套接字类型,如 SOCK_STREAM、SOCK_DGRAM 等;
    unsigned long       flags;   // 套接字标志,如 O_NONBLOCK、O_ASYNC 等;

    /* Protocol specific elements of the socket */
    struct proto        *ops;      // 协议特定操作函数集;
    struct net_device   *sk_net;   // 套接字所在的网络设备;

    /* Memory allocation cache */
    kmem_cache_t        *sk_slab;  // 套接字内存分配缓存;

    /* Socket state */
    atomic_t            refcnt;    // 套接字引用计数;
    struct mutex        sk_lock;   // 套接字锁,用于保护套接字状态的一致性;

    /* Send and receive buffers */
 struct sk_buff_head sk_receive_queue;  // 接收队列,保存了等待接收的数据包;
    struct sk_buff_head sk_write_queue;    // 发送队列,保存了等待发送的数据包;
    struct sk_buff      *sk_send_head;     // 发送缓冲区的头指针;
    struct sk_buff      *sk_send_tail;     // 发送缓冲区的尾指针;

    /* Receive queue */
    struct sk_buff      *sk_receive_skb;   // 当前正在接收的数据包;

    /* Transport specific fields */
    __u32           sk_priority;           // 套接字优先级;
    struct dst_entry    *sk_dst_cache;     // 缓存的目标设备;
    struct dst_entry    *sk_dst_pending_confirm;
    struct flowi        sk_fl;             // Flowi 结构体,保存了套接字相关的流信息;
    struct sk_filter    *sk_filter;        // 过滤器;
    struct sk_buff_head sk_async_wait_queue;  // 异步等待队列;

    /* Socket buffer allocations */
    unsigned long       sk_wmem_alloc;  // 发送缓冲区已分配的内存;
    unsigned long       sk_omem_alloc;

    /* User and kernel buffers */
    struct socket_wq    *sk_wq;   // 套接字等待队列;
    struct page_frag    sk_frag;  // 内存分配器的页片段;
    int         sk_forward_alloc; // 前向分配的字节数;
    int         sk_rxhash;        // 套接字是否支持接收哈希。
};

Protocol Family

Socket 支持广泛的 PFs,主要有以下 4 类:

  1. PF_INETv4v6 sockets(IP Socket):基于 IPv4v6 网络层协议,支持 TCP、UDP 传输层协议。
  • SOCK_STREAM:TCP 字节流式传输。
  • SOCK_DGRAM:UDP 数据包式传输。
  • SOCK_RAW:原始套接字,可以处理 IPv4、ICMP、IGMP 等报文,常用于网络监听、检验新的协议或者访问新的设备。
  1. PF_PACKET sockets(Packet Socket):基于 Device Driver(设备驱动),支持对底层数据包的捕获和注入,常用于网络安全、网络监测等场景,例如:网络嗅探、协议分析、数据包过滤等。

  2. PF_NETLINK sockets(Netlink Socket):支持 Kernel Space 和 User Space 之间的通信,常用于网络管理和网络监测等场景,例如:获取内核中的网络信息、配置内核的网络参数、监控网络状态等。

  3. PF_UNIX sockets(UNIX socket):用于 Unix-like 系统中的多进程之间通信。

值得注意的是,虽然不同的协议族都使用了同一套 Socket API,但也可能会存在一些特有的函数或者数据结构,用于实现协议族特有的功能。例如:

  • PF_PACKET 协议族可以使用 pcap 库来进行网络数据包捕获和注入;

  • PF_NETLINK 协议族可以使用 netlink 库来进行内核和用户空间之间的通信。

但是,这些特有的函数和数据结构通常不会影响套接字编程接口的基本使用方式和语法。

img3

VFS Layer

VFS Layer 属于 Linux VFS sub-system(虚拟文件子系统),提供了一组通用的 Linux File System Call APIs(SCI),使得 Application 可以使用相同的 API 来完成文件 I/O

当 Application 使用 Socket API 发送或接收数据时,Socket Abstraction Layer 会借助 VFS Layer 来完成 Socket File System 的管理。例如:

  • Application 调用 Socket API socket() 创建 socket 时:在 VFS I/O Layer 中,Socket FD 文件句柄被创建。
  • Application 调用 Socket API close() 关闭 socket 时:在 VFS I/O Layer 中,文件句柄被释放,并释放相关资源。

PF_INET sockets

PF_INET sockets 基于 IPv4v6 网络层协议,支持 TCP、UDP 等传输层协议。是 Linux 网络编程中最常用到的协议族。

img4

1、创建套接字

socket()

函数功能:创建一个新的套接字,返回一个 int 类型的套接字文件描述符(作为 Linux 文件操作的句柄),用于后续的网络连接操作。

函数原型

  • af 参数:指定 Socket AF(Address Family,地址族),对于 PF_INETv4v6 sockets 而言,可选:

    • AF_INET
    • AF_INET6
  • type 参数:指定数据传输方式,可选:

    • SOCK_STREAM(面向连接的 TCP)
    • SOCK_DGRAM(无连接的 UDP)
    • SOCK_RAW(原始 IP 数据包)
  • protocol:指定具体的传输层协议,可选:

    • IPPROTO_TCP
    • IPPTOTO_UDP
  • 函数返回值

    • 成功:返回 Socket fd。
    • 失败:返回 -1。
  • #include <sys/socket.h>
    
    int socket(int af, int type, int protocol);
    

示例

// 创建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

// 创建 UDP 套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

setsockopt()

函数功能:用于设置 Socket 的选项值。

函数原型

  • sockfd 参数:指定 socket fd。

  • level 参数:指定选项的协议层,可选 SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP 等。

  • optname 参数:指定要设置的选项名。

    • SO_REUSEADDR:int 类型,表示重用 IP 地址
    • SO_KEEPALIVE:int 类型,用于启用/禁用 Keepalive(保持连接)功能。
    • SO_LINGER:struct linger 类型,用于指定关闭套接字时的行为
    • TCP_NODELAY:int 类型,用于禁用 Nagle 算法,从而实现数据的实时传输
  • optval 参数:指定存放选项值的缓冲区入口。

  • optlen 参数:指定选项值缓冲区的长度。

  • 函数返回值

    • 成功:0。
    • 失败:-1,并设置了 errno 错误码。
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

2、配置套接字

bind()

将 Socket 与主机中的某个 IP:Port 绑定起来。

函数作用:将套接字与一个本地 IP:Port 绑定。通常用于服务端,以便在本地监听网络连接。函数原型

  • sock 参数:指定 Server socket 文件描述符。

  • addr 参数:指定 Server sockaddr 结构体变量,指针类型。

  • addrlen 参数:指定 addr 变量的大小,使用 sizeof() 计算得出。

  • 函数返回值

    • 失败:返回 -1。
#include <sys/socket.h>

int bind(int sock, struct sockaddr *addr, socklen_t addrlen); 

示例

int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

struct sockaddr_in tcp_socket_addr;                    // 定义 Server Socket Address
memset(&tcp_socket_addr, 0, sizeof(tcp_socket_addr));  // 初始化结构体内存

tcp_socket_addr.sin_family = PF_INET;
tcp_socket_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 定义本地 IP 地址
tcp_socket_addr.sin_port = htons(1314);                    // 定义本地 Port

bind(tcp_socket, (sockaddr *)&tcp_socket_addr, sizeof(sockaddr));  // 绑定

其中 sockaddr_in 结构类型的声明如下。使用时,需要先定义并初始化 sockaddr_in,然后再将它强制转化成 sockaddr 来使用。2 个结构体长度均为 16B,其中,sockaddr_in.sin_family 的 2B 存入 sockaddr.sa_family,剩下的 14B 存入 sockaddr.sa_data。

这样做是为了在后续的各种操作中可以更方便的处理 IP 地址和 Port 号。

#include <arpa/inet.h>

struct in_addr {
 unsigned long a_addr;
}

struct sockaddr_in {
 unsigned short    sin_family;     // 地址类型(2B)
 unsigned short int  sin_port;     // 端口号(2B)
 struct in_addr      sin_addr;    // IP 地址(4B)
 unsigned char       sin_zero[8];  // 填充空间(8B)
}

struct sockaddr {
      unsigned short  sa_family; // 地址类型(2B)
      char            sa_data[14]; // 协议地址(14B)
 }

另外,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;  // 具体的 IPv6 地址
    uint32_t sin6_scope_id;     // 接口范围 ID
};

如果 sock_addr.sin_port 赋值为 0,或者没有调用 bind(),而直接调用 listen(),那么 Kernel 会自动为 Socket 临时分配一个 Port。此时需要调用 getsockname() 来获取具体的端口信息。

getsockname(httpd, (struct sockaddr *)&name, &namelen);
ntohs(name.sin_port);

listen()

函数作用:开始监听来自远程主机的连接请求。通常用于服务器端,在套接字上等待来自客户端的连接请求。

函数原型

  • sock 参数:指定需要进入监听状态的 Server socket 文件描述符。

  • backlog 参数:指定请求队列的最大长度,当队列满了之后,就不再接收请求。

  • 函数返回值

    • 失败:返回 -1。
#include <sys/socket.h>

int listen(int sock, int backlog);

connect()

函数作用:建立与远程主机的连接。通常用于客户端,以便连接到远程服务器。函数原型

  • sock 参数:指定 Client socket 文件描述符。

  • serv_addr 参数:指定 Server sockaddr 结构体变量,指针类型。

  • addrlen 参数:指定 addr 变量的大小,使用 sizeof() 计算得出。

  • 函数返回值

    • 失败:返回 -1。
#include <sys/socket.h>

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 

示例

int cli_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

struct sockaddr_in server_sock_addr;                     // 定义 Server Socket Address
memset(&server_sock_addr, 0, sizeof(server_sock_addr));  // 初始化结构体内存

server_sock_addr.sin_family = PF_INET;
server_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 定义本地 IP 地址
server_sock_addr.sin_port = htons(1314);                    // 定义本地 Port

connect(cli_socket, (sockaddr *)&server_sock_addr, sizeof(sockaddr));

accept()

函数作用:接受一个连接请求,返回一个新的、表示客户端的 Socket 文件描述符,作为服务端和客户端之间发送与接收操作的句柄。通常用于服务器端,用于接收客户端的连接请求。

函数原型

  • sock 参数:指定 Server socket 文件描述符。

  • addr 参数:指定 Client sockaddr 结构体变量,指针类型。

  • addrlen 参数:指定 addr 变量的大小,使用 sizeof() 计算得出。

  • 函数返回值

    • 成功:返回 Client socket fd。
    • 失败:返回 -1。
#include <sys/socket.h>

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);

示例

// 返回一个新的套接字,用于后续的发送和接收
int cli_socket = accept(server_socket, (sockeraddr *)&cli_socket_addr, &len);

getnameinfo()

函数作用:用于将一个 Sock Addr 转换为对应的 Hostname 或 Service name,以便于记录日志或者显示给用户。

函数原型

  • addr:表示需要转换的 Sock Addr;

  • addrlen:表示该 Socket addr址的长度;

  • host:输出 Hostname 的存储空间。

  • serv:输出 Service name 的存储空间。

  • hostlen:Hostname 存储空间的大小。

  • servlen:Service name 存储空间的大小。

  • flags:标志参数,通常设置为 0。

  • 函数返回值

    • 成功:返回 0。
    • 失败:返回非 0,并更新 errno 全局变量。
#include <sys/socket.h>
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
                char *host, socklen_t hostlen,
                char *serv, socklen_t servlen, int flags);

3、数据传输

recv() 和 send()

recv() 和 send() 函数,用于在 TCP Socket 中进行数据读写,属于阻塞式 I/O(Blocking I/O)模式,即:如果没有可读数据或者对端的接收缓冲区已满,则函数将一直等待直到有数据可读或者对端缓冲区可写。

recv():从套接字接收数据。

  • sockfd 参数:指定要接收 TCP 数据的 Socket 文件描述符。

  • buf 参数:指定接收数据缓冲区的入口地址。

  • len 参数:指定要接收的数据的 Byte 数目。

  • flags:指定接收数据时的选项,常设为 0。

  • 函数返回值

    • 成功:返回接收的字节数。
    • 失败:返回 -1。
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

send():向套接字发送数据。

  • sockfd 参数:指定要发送 TCP 数据的 Socket 文件描述符。

  • buf 参数:指定发送数据缓冲区入的口地址。

  • len 参数:指定要发送数据的 Byte 数目。

  • flags 参数:指定发送数据时的选项,常设为 0。

  • 函数返回值

    • 成功:返回发送的字节数。
    • 失败:返回 -1。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

recvfrom() 和 sendto()

recvfrom() 和 sendto() 函数,用于在 UDP Socket 中进行数据读写以及获取对端地址。这两个函数在使用时需要指定对端的 IP:Port。

recvfrom()

  • sock 参数:指定要接收 UDP 数据的 Socket 文件描述符。

  • buf 参数:指定接收数据缓冲区的入口地址。

  • nbytes 参数:指定要接收数据的 Byte 数目。

  • flags 参数:指定接收数据时的选项,常设为 0。

  • from 参数:指定源地址 sockaddr 结构体变量的地址。

  • addrlen 参数:指定 from 参数使用的长度,使用 sizeof() 获取。

  • 函数返回值

    • 成功:返回接收的字节数。
    • 失败:返回 -1。
#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);

sendto()

  • sock 参数:指定要发送 UDP 数据的 Socket 文件描述符。

  • buf 参数:指定发送数据缓冲区的入口地址。

  • nbytes 参数:指定要发送数据的 Byte 数目。

  • flags 参数:指定发送数据时的选项,常设为 0。

  • to 参数:指定目标地址 sockaddr 结构体变量的地址。

  • addrlen 参数:指定 to 参数使用的长度,使用 sizeof() 获取。

  • 函数返回值

    • 成功:返回发送的字节数。
    • 失败:返回 -1。
#include <sys/socket.h>

ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);

recvmsg() 和 sendmsg()

recvmsg() 和 sendmsg() 函数,用于在 TCP 和 UDP Socket 中进行数据读写,不仅可以读写数据,还可以读写对端地址、辅助数据等信息。

recvmsg()

  • sock 参数:指定要接收 TCP 或 UDP 数据的 Socket 文件描述符。

  • msg 参数:指示将接收的数据存储到 msghdr 结构体中。

  • flags 参数:支持函数的行为,可选 0 或者 MSG_DONTWAIT 等标志位。

  • 函数返回值

    • 成功:返回接收的字节数。
    • 失败:返回 -1。
#include <sys/socket.h>

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

sendmsg()

  • sock 参数:指定要发送 TCP 或 UDP 数据的 Socket 文件描述符。

  • msg 参数:指示 msghdr 结构体中包含了要发送的数据、数据长度等信息。

  • flags 参数:支持函数的行为,可选 0 或者 MSG_DONTWAIT 等标志位。

  • 函数返回值

    • 成功:返回发送的字节数。
    • 失败:返回 -1。
#include <sys/socket.h>

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

msghdr 结构体定义如下

struct msghdr {
    /* 指定接收或发送数据的对端地址,可以为 NULL 或 0,表示不需要使用对端地址。*/
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */

    /* 指定接收或发送数据的缓冲区和缓冲区大小,可以使用多个缓冲区同时接收或发送数据。*/
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */

 /* 指定一些附加的控制信息,可以为 NULL 或 0。*/
    void         *msg_control;    /* ancillary data, see below */
    size_t        msg_controllen; /* ancillary data buffer len */

 /* 指定函数的行为,例如是否需要接收带外数据等。*/
    int           msg_flags;      /* flags on received message */
};

flags 参数类型

  • MSG_PEEK:允许从接收队列中查看数据而不将其删除。这意味着,如果接收队列中有数据,recv() 函数将返回数据的一个副本,但是该数据仍将留在接收队列中。这对于查看接收队列中的数据而不实际处理它们非常有用。此外,使用 MSG_PEEK 选项,我们可以检查套接字缓冲区中是否有足够的数据可供读取,以便稍后调用 recv() 函数。
  • MSG_WAITALL:如果套接字缓冲区中没有足够的数据,则 recv() 函数将一直等待,直到收到请求的数据量。
  • MSG_DONTWAIT:指定此标志后,recv() 函数将立即返回,即使没有收到数据也不会阻塞。如果没有数据可用,则 recv() 将返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。
  • MSG_OOB:用于处理带外数据,即紧急数据。带外数据不遵循正常的传输控制协议(如 TCP),可以使用此标志将其标记为紧急数据并将其与其他数据分开处理。
  • MSG_TRUNC:如果接收缓冲区中的数据比接收缓冲区长度长,则截断数据并返回。
  • MSG_CTRUNC:如果接收缓冲区中的控制消息(例如带外数据或错误消息)比接收缓冲区长度长,则截断消息并返回。

4、关闭套接字

close()

函数作用:关闭套接字连接。函数原型

  • fd:指定要关闭的 Socket 的文件描述符。

  • 函数返回值

    • 失败:返回 -1。
#include <unistd.h>

int close(int fd);

服务端

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

#include <arpa/inet.h>
#include <sys/socket.h>


#define ERR_MSG(err_code) do {                                     \
    err_code = errno;                                              \
    fprintf(stderr, "ERROR code: %d \n", err_code);                \
    perror("PERROR message");                                      \
} while (0)

const int BUF_LEN = 100;


int main(void)
{
    /* 配置 Server Sock 信息。*/
    struct sockaddr_in srv_sock_addr;
    memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
    srv_sock_addr.sin_family = AF_INET;
    srv_sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 即 0.0.0.0 表示监听本机所有的 IP 地址。
    srv_sock_addr.sin_port = htons(6666);

    /* 创建 Server Socket。*/
    int srv_socket_fd = 0;
    if (-1 == (srv_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) {
        printf("Create socket file descriptor ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }
    /* 设置 Server Socket 选项。*/
    int optval = 1;
    if (setsockopt(srv_socket_fd,
                   SOL_SOCKET,    // 表示套接字选项的协议层。
                   SO_REUSEADDR,  // 表示在绑定地址时允许重用本地地址。这样做的好处是,当服务器进程崩溃或被关闭时,可以更快地重新启动服务器,而不必等待一段时间来释放之前使用的套接字。
                   &optval,
                   sizeof(optval)) < 0)
    {
        printf("Set socket options ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 绑定 Socket 与 Sock Address 信息。*/
    if (-1 == bind(srv_socket_fd,
                   (struct sockaddr *)&srv_sock_addr,
                   sizeof(srv_sock_addr)))
    {
        printf("Bind socket ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 开始监听 Client 发出的连接请求。*/
    if (-1 == listen(srv_socket_fd, 10))
    {
        printf("Listen socket ERROR.\n");
        ERR_MSG(errno);
        exit(EXIT_FAILURE);
    }

    /* 初始化 Client Sock 信息存储变量。*/
    struct sockaddr cli_sock_addr;
    memset(&cli_sock_addr, 0, sizeof(cli_sock_addr));
    int cli_sockaddr_len = sizeof(cli_sock_addr);

    int cli_socket_fd = 0;

    int recv_len = 0;
    char buff[BUF_LEN] = {0};

    /* 永远接受 Client 的连接请求。*/
    while (1)
    {
        if (-1 == (cli_socket_fd = accept(srv_socket_fd,
                                          (struct sockaddr *)(&cli_sock_addr),  // 填充 Client Sock 信息。
                                          (socklen_t *)&cli_sockaddr_len)))
        {
            printf("Accept connection from client ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 接收指定 Client Socket 发出的数据,*/
        if ((recv_len = recv(cli_socket_fd, buff, BUF_LEN, 0)) < 0)
        {
            printf("Receive from client ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }
        printf("Recevice data from client: %s\n", buff);

        /* 将收到的数据重新发送给指定的 Client Socket。*/
        send(cli_socket_fd, buff, recv_len, 0);
        printf("Send data to client: %s\n", buff);

        /* 每处理完一次 Client 请求,即关闭连接。*/
        close(cli_socket_fd);
        memset(buff, 0, BUF_LEN);
    }

    close(srv_socket_fd);
    return EXIT_SUCCESS;
}

客户端

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

#include <arpa/inet.h>
#include <sys/socket.h>


#define ERR_MSG(err_code) do {                                     \
    err_code = errno;                                              \
    fprintf(stderr, "ERROR code: %d \n", err_code);                \
    perror("PERROR message");                                      \
} while (0)

const int BUF_LEN = 100;


int main(void)
{
    /* 配置 Server Sock 信息。*/
    struct sockaddr_in srv_sock_addr;
    memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
    srv_sock_addr.sin_family = AF_INET;
    srv_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    srv_sock_addr.sin_port = htons(6666);

    int cli_socket_fd = 0;
    char send_buff[BUF_LEN];
    char recv_buff[BUF_LEN];

    /* 永循环从终端接收输入,并发送到 Server。*/
    while (1) {

        /* 创建 Client Socket。*/
        if (-1 == (cli_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
        {
            printf("Create socket ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 连接到 Server Sock 信息指定的 Server。*/
        if (-1 == connect(cli_socket_fd,
                          (struct sockaddr *)&srv_sock_addr,
                          sizeof(srv_sock_addr)))
        {
            printf("Connect to server ERROR.\n");
            ERR_MSG(errno);
            exit(EXIT_FAILURE);
        }

        /* 从 stdin 接收输入,再发送到建立连接的 Server Socket。*/
        fputs("Send to server> ", stdout);
        fgets(send_buff, BUF_LEN, stdin);
        send(cli_socket_fd, send_buff, BUF_LEN, 0);
        memset(send_buff, 0, BUF_LEN);

        /* 从建立连接的 Server 接收数据。*/
        recv(cli_socket_fd, recv_buff, BUF_LEN, 0);
        printf("Recevice from server: %s\n", recv_buff);
        memset(recv_buff, 0, BUF_LEN);

        /* 每次 Client 请求和响应完成后,关闭连接。*/
        close(cli_socket_fd);
    }

    return EXIT_SUCCESS;
}

测试

编译:

$ gcc -g -std=c99 -Wall tcp_server.c -o tcp_server
$ gcc -g -std=c99 -Wall tcp_client.c -o tcp_client

运行:

  1. 先启动 TCP Server:
$ ./tcp_server
  1. 查看监听 Socket 是否绑定成功:

    $ netstat -lpntu | grep 6666
    tcp        0      0 0.0.0.0:6666            0.0.0.0:*               LISTEN      28675/./tcp_server
    
  2. 启动 TCP Client

$ ./tcp_client