重生之我是网络编程高手之网络套接字!!!

89 阅读37分钟

目录

1、前言

2、必备知识

​ 2.1 认识端口号(port)

​ 2.2 重温 UDP 和 TCP

​ 2.3 网络字节序

3、socket api

​ 3.1 网络字节序和主机字节序转化

​ 3.1.1 htonl() 和 htons()

​ 3.1.2 ntohl() 和 ntohs()

​ 3.2 socket 常见接口

​ 3.2.1 通用 socket 接口:socket()、bind()

​ 3.2.2 用于 TCP 的 socket 接口:listen()、accept()、connect()

4、基于 socket api 实现一个十分十分简单的 UDP 服务器和客户端(重点)

​ 4.1 客户端

​ 4.2 服务端

​ 4.2.1 version 1:英文翻译的服务器

​ 4.2.2 version 2:执行命令解释的服务器

​ 4.2.3 version 3:聊天室版的服务器

5、基于 socket api 实现一个十分简单的 TCP 服务器

​ 5.1 准备工作

​ 5.2 客户端

​ 5.3 服务端

​ 5.3.1 version 1:单进程版的服务器(最基础)

​ 5.3.2 version 2:多进程版的服务器(ez)

​ 5.3.3 version 3:多线程版的服务器(正常)

​ 5.3.4 version 4:线程池版的服务器(困难)

​ EXTRA 5.3.5 version 5:守护进程化的服务器

6、TCP 协议通信原理

​ 6.1 三次握手

​ 6.2 四次握手

1、前言

在网络编程中,套接字这个概念是无论如何都绕不开的:正是因为套接字,如今的网络通信才能如此高效!

本篇博客将会引领各位一起由浅入深地去一步一步挖掘套接字以及周边相关知识!


本篇重点

  • 认识端口号,对 ip 地址有一个新的理解

  • 学会 socket api 相关的用法

  • 借助 socket api 实现一个简单 UDP 服务器 和 TCP 服务器

  • 理解 TCP 协议背后的部分行为——三次握手,四次挥手


2、必备知识

疑问:已知 ip 地址标定了广域网中唯一的主机,但是只有 ip 地址就能够进行网络通信吗?

思考助手:

​ 我们先思考一个问题:我们到某处地方旅游的时候,我们重点是哪个?到这块地方 还是 旅游 ?我们蒙上眼睛都可以知道肯定是 旅游 啊!!!而到这地方是为了 旅游 的一个手段!现在再重新回到这个问题上的时候,答案应该也就迎刃而解了,在进行网络通信的时候,将数据发到对方主机上只是一种手段,而我们真正的目的肯定是对这份数据进行处理转化成我们—— 人 能够看懂的信息!!!

回复:不能,但是加上 端口号 就可以实现这个目的了!!!

2.1 认识端口号(port)

基本认识:端口号是属于 传输层 的内容,是一个 16 字节大小的整数( 数据类型为uint16_t )。

作用:用于标识一个进程(也可以说成 服务 )——当数据通过网络被发送到对方主机的时候,对方主机要通过这个端口号所对应的进程对这份数据进行处理。

重点:通过 ip + 端口号 的方式可以唯一地标定网络中的某一项服务——这里就可以引出我们对于 ip 的一个新的理解

注意:一个端口号只能由一个进程进行绑定——这里就呼应了上面的 唯一

理解助手:

​ 假如此时一个端口号被多个进程所绑定,那么当对方主机进行网络通信向我们主机发送数据时,我们的主机将不知道应该将这份数据传给哪个进程。

思考:

​ 提问:一个进程能绑定多个端口号吗?

​ 回复:可以,因为这并不影响后续主机通过端口号将数据交付给进程进行处理!!!

举例解释:

​ 就像我们熟知的 10086,这里面就有许多不同的职工了,每一个职工都有他们自己的独一份的工号,每一个职工都有不同的业务。所以当我们要进行不同的业务工作的时候,就会有不同的职工来帮助我们。

套接字 就是 ip + port统称!!!

2.2 重温 UDP 和 TCP

UDP 的特征:

  • 不可靠
  • 无连接
  • 面向数据报

TCP 的特征:

  • 可靠
  • 连接
  • 面向字节流

上面所展示的这两种协议特征在码字的过程中可能会比较明显。

2.3 网络字节序

知识小助手:

​ 字节序分为了两种:

​ 1、大端: 高位字节放到低地址处,低位字节放到高地址处。

​ 2、小端: 高位字节放到高地址处,低位字节放到低地址处。

记忆小助手:

​ 小端小小小。

编写一段检测代码:

#include <stdio.h>

void Endianness()
{
    int a = 1;
    int *p = &a;
    
    if(*(char*)p)
    {
        printf("小端\n");
    }
    else
    {
        printf("大端\n");
    }
}

int main()
{
    Endianness();

    return 0;
}

​ 运行上述代码便可以得到我们的主机的存储方式。

不同的主机所对应的字节序可能不同,有的可能主机可能是大端,有的主机可能是小端,而为了保证双方通信的主机最后看到的数据是一致,所以规定网络中的数据都要以大端的方式对数据进行存储,这是一种简单粗暴但是十分高效的方法!

运行逻辑:在进行网络通信时,发送方 发送 数据时,先对自身数据存储方式进行检测,假如是大端则直接发送,假如不是则先以大端的存储方式转化数据,然后再进行发送,接收方 接收 数据时,此时也需要先对自身数据存储方式进行检测,假如是大端则直接接受,假如不是则先以大端的存储方式转化数据,然后再进行接收!!!


3、socket api

有了上面的必备知识,下面的内容也就水到渠成了!!!

3.1 网络字节序和主机字节序转化

趁热打铁,既然网络字节序和主机字节序之间需要发生相互转化,那么肯定需要有相应的接口用来进行转化!

需要包含的头文件#include <arpa/inet.h>

包括的函数

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

理解助手:

​ 字母 h 指的是 host(主机)的意思,字母 n 指的是 net(网络)的意思,字母 l 指的是 long(32 位整型) 的意思,字母 s 指的是 short(16 位整型)的意思。


3.1.1 htonl() 和 htons()

这俩函数前面整体下来意思就是:由 主机序列 转化为 网络序列

根据这个意思,我们可以很轻松地推测出这两函数的使用场景了:发送方向网络中发送数据。

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);

讲解思路:

​ 这里我以 htonl 为例子进行讲解,通过 htonl 就能轻易地推出 htons 的各个部分了。

传入的参数:

传参类型uint32_t,指的我们需要传入一个 32 位的整形数据进去。

返回值:

返回类型uint32_t,指的最后会将传入的这个 32 位整形数据以 大端 的方式排列后的 32 位整型数据返回。


3.1.2 ntohl() 和 ntohs()

这俩函数前面整体下来意思就是:由 网络序列 转化为 主机序列

根据这个意思,我们也可以很轻松地推测出这两函数的使用场景了:接收方从网络中获取数据。

uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

讲解思路:

​ 这里我以 ntohl 为例子进行讲解,通过 ntohl 就能够轻易地退出 ntohs 的各个部分了。

传入的参数:

传参类型uint32_t,指的我们需要传入一个 32 位的整形数据进去。

返回值:

返回类型uint32_t,指的最后会将传入的这个 32 位整形数据以 大端 的方式排列后的 32 位整型数据返回。


3.2 socket 常见接口

使用 socket 的接口时所需要的头文件:#include <sys/socket.h>

注意:由于传输层中 TCPUDP 这两种协议特点不同,所以这两种不同的协议所使用的接口可能存在差异。

3.2.1 通用 socket 接口:socket()、bind()

socket()

int socket(int domain, int type, int protocol);

官方描述:socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.

翻译:socket() 创建一个用于通信的端点,并返回一个引用该端点的文件描述符。

知识助手:

​ 这里的返回值暗示了网络编程中的操作跟文件操作是一样的,所以才会将文件描述符返回。


domain:The domain argument specifies a communication domain.——这是官方给的解释,翻译过来就是:domain 参数指定通信域。

理解助手:

​ 通俗地理解就是这个参数是用来表示用什么媒介来通信。

而我们需要对这个参数填入以下其中一个的宏:

	   Name         Purpose                                    Man page
---------------------------------------------------------------------------------------------------------------------------	   
       AF_UNIX      Local communication                        unix(7)
       AF_LOCAL     Synonym for AF_UNIX
       AF_INET      IPv4 Internet protocols                    ip(7)
       AF_AX25      Amateur radio AX.25 protocol               ax25(4)
       AF_IPX       IPX - Novell protocols
       AF_APPLETALK AppleTalk                                  ddp(7)
       AF_X25       ITU-T X.25 / ISO/IEC 8208 protocol         x25(7)
       AF_INET6     IPv6 Internet protocols                    ipv6(7)
       AF_DECnet    DECet protocol sockets
       AF_KEY       Key  management protocol, originally de‐
                    veloped for usage with IPsec
       AF_NETLINK   Kernel user interface device               netlink(7)
       AF_PACKET    Low-level packet interface                 packet(7)
       AF_RDS       Reliable Datagram Sockets (RDS) protocol   rds(7)
                                                               rds-rdma(7)
       AF_PPPOX     Generic PPP transport layer, for setting
                    up L2 tunnels (L2TP and PPPoE)
       AF_LLC       Logical link control  (IEEE  802.2  LLC)
                    protocol
       AF_IB        InfiniBand native addressing
       AF_MPLS      Multiprotocol Label Switching
       AF_CAN       Controller  Area  Network automotive bus
                    protocol
       AF_TIPC      TIPC, "cluster domain sockets" protocol
       AF_BLUETOOTH Bluetooth low-level socket protocol
       AF_ALG       Interface to kernel crypto API
       AF_VSOCK     VSOCK  (originally  "VMWare   VSockets")   vsock(7)
                    protocol for hypervisor-guest communica‐
                    tion
       AF_KCM       KCM  (kernel connection multiplexer) in‐
                    terface
       AF_XDP       XDP (express data path) interface

理解助手:

​ 第一个宏 AF_UNIX ,后面对应的解释为:本地通信。指的就是我们需要本地通信的时候就填这个宏进去。

​ 第二个宏 AF_LOCAL,后面对应的解释为:AF_LOCAL 这个宏的同义词。也就是说底层里这个 AF_LOCAL 就是 AF_UNIX 重定义的。

​ 借助这俩个宏,我们就可以大致理解这个参数的意义了。

由于我们的目标是网络通信,所以直接锁定目标:AF_INET

AF_INET 的官方解释:IPv4 的互联网协议。也就是当我们需要进行基于 IPv4 的网络通信的时候,我们就要将 AF_INET 填入。


type:The socket has the indicated type, which specifies the communication semantics.——这是官方的解释,翻译过来就是:套接字具有指示的类型,该类型指定通信语义。

我们需要向这个填入以下的其中一个参数:

	   SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission 						   mechanism may be supported.

       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

       SOCK_SEQPACKET  Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed 						   maximum length; a consumer is required to read an entire packet with each input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.

       SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

先以 SOCK_STREAM 为例,官方给出的解释翻译过来:提供有序、可靠、双向、基于连接的字节流。可能支持带外数据传输机制。——看完这个解释,我们可以显然的感受到这个宏跟 TCP 传输协议是脱不开干系的!

然后就是 SOCK_DGRAM,官方给出的解释翻译过来:支持数据报(固定最大长度的无连接、不可靠的消息)。——看完这个解释,我们也可以显然的察觉到这个宏跟 UDP 传输协议是脱不开干系的!

注意:在这里,我们仅需要注意这两个宏即可。

知识小助手:

​ 从 Linux 2.6.27 开始,type 参数还有第二个作用:除了指定套接字类型外,它还可以包括以下任何值的按位或,以修改 socket()的行为:

 SOCK_NONBLOCK   Set the O_NONBLOCK file status flag on the open file description (see open(2)) referred to by the new file 					 descriptor. Using this flag saves extra calls to fcntl(2) to achieve the same result.
  			
 SOCK_CLOEXEC    Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the 						       O_CLOEXEC flag in open(2) for reasons why this may be useful.

​ 第一个宏 SOCK_NONBLOCK 作用:将对应套接字打开的文件描述符设置为非阻塞的状态。

​ 第二个宏 SOCK_CLOEXEC 作用:在新文件描述符上设置 close-on-exec (FD_CLOEXEC) 标志——当文件描述符使用了 exec() 系列的函数时会自动关闭该 文件描述符。


protocal:The protocol specifies a particular protocol to be used with the socket. Normally only a single protocol exists to support a particular socket type within a given protocol family, in which case protocol can be specified as 0. However, it is possible that many protocols may exist, in which case a particular protocol must be specified in this manner. The protocol number to use is specific to the “communication domain” in which communication is to take place.——这是官方解释,翻译过来就是:该协议指定要与套接字一起使用的特定协议。通常,只有一个协议存在来支持给定 Socket 中的特定 socket 类型 protocol family,在这种情况下,protocol 可以指定为 0。但是,可能存在许多协议,在这种情况下,特定协议必须以这种方式指定。要使用的协议号特定于要进行通信的 “通信域”。

补充助手:

​ 其实大多时候这个参数设为 0 即可。


返回值

官方解释:On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set to indicate the error.

翻译:成功后,将返回新套接字的文件描述符。出错时,返回 -1,并设置 errno 以指示错误。


bind()

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

官方描述:When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd.

翻译:当使用 socket(2) 创建套接字时,它存在于名称空间(地址系列)中,但没有为其分配地址。bind() 分配 addr 指定的地址到文件描述符 sockfd 引用的套接字中。

通俗理解:将 addr 这个指针所指向的 ip 地址和 port 端口号(套接字)与 sockfd 进行绑定——sockfd 这个文件描述符后续可以理解为就是这个 套接字

知识助手:

​ ​ 在进行分配地址的时候,就需要用到 struct sockaddr,但是这个结构体本身的作用就是用来分配空间,内部没有别的多余成员,而真正实现存储数据的是后面的 struct sockaddr_in 和 struct sockaddr_un 这两个结构体,所以当我们传参的时候需要进行 强制类型转化,也是这种方式才能实现在 C 语言的多态,借此从而达到一种 解耦 的效果。

屏幕截图 2025-06-12 114119.png sockfd:The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.——这是官方解释,翻译过来:sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字

理解助手:

​ 本质就是因为调用 sock() 这个接口的时候,没有给 sock() 分配的这个文件描述符 sockfd 分配空间,所以需要通过 bind() 这个接口来完成这个工作。


addr:bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr.——这是官方解释,翻译过来:bind() 分配 addr 指定的地址到文件描述符 sockfd 引用的套接字中。addrlen 指定 addr 指向的地址结构的大小(以字节为单位)。

知识助手:

​ struct sockaddr:

struct sockaddr {
    sa_family_t sa_family;  //地址族
    char        sa_data[14];//地址信息:包括了目标ip和port
}

​ struct addr_in:

struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.(端口号)*/
    struct in_addr sin_addr;		/* Internet address. (ip地址)*/

    /* Pad to size of `struct sockaddr'. (用于填充的数据)*/
    unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };

​ struct in_addr:

typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

addrlen: addrlen specifies the size, in bytes, of the address structure pointed to by addr.——这是官方解释,翻译过来:addrlen 指定 addr 指向的地址结构的大小(以字节为单位)。

知识助手:

​ socklen_t 底层是由 unsigned int 重命名过来的。

忧郁蓝调!!!案件回溯:

typedef __socklen_t socklen_t;
__STD_TYPE __U32_TYPE __socklen_t;
#define __U32_TYPE		unsigned int

返回值

官方解释:On success, zero is returned. On error, -1 is returned, and errno is set to indicate the error.

翻译:成功后,返回 0。出错时,返回 -1,并设置 errno 以指示错误。


3.2.2 用于 TCP 的 socket 接口:listen()、accept()、connect()

listen()

int listen(int sockfd, int backlog);

官方描述:listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).

翻译:listen() 将 sockfd 引用的套接字标记为被动套接字,即,作为将使用 accept(2) 接受传入连接请求的套接字。

通俗理解:listen() 将 sockfd 这个套接字设置为一个用于监听的套接字,监听是否有请求连接的套接字。

理解助手: listen() 这个接口就相当于现实中的餐厅门口的服务员,用来拉客的,假如有客人要进店,就会拉这名客人到店内,然后招呼好伙计来进行后续点菜等服务。


sockfd:The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.——这是官方解释,翻译过来:sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字


backlog:The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.——这是官方解释,翻译过来:backlog 参数定义了 sockfd 的待处理连接队列可以增长到的最大长度。


返回值

官方解释:On success, zero is returned. On error, -1 is returned, and errno is set to indicate the error.

翻译:成功后,返回 0。出错时,返回 -1,并设置 errno 以指示错误。


accept()

int accept(int sockfd, struct sockaddr *_Nullable restrict addr, socklen_t *_Nullable restrict addrlen);

官方描述: The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.

翻译:accept() 系统调用与基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)一起使用。它提取待处理队列中的第一个连接请求侦听套接字 sockfd 的连接创建一个新的已连接套接字,并返回引用该套接字的新文件描述符。

通俗理解:accept() 这个系统调用可以 获取 sockfd 监听到的套接字,并将这个套接字以 文件描述符 的方式返回。

理解助手:

​ 上面的 listen() 相当于餐厅门口的服务员的话,这个 accept() 就相当于店内的服务员了,对客人进行后续的服务。——因为网络编程中的操作其实还是文件操作,而 accept() 会将这个文件描述符返回,后续对对方的套接字的操作,都要用这个 套接字 来完成。


sockfd:The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.——这是官方解释,翻译过来:sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字


addr:The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket, as known to the communications layer. The exact format of the address returned addr is determined by the socket's address family. When addr is NULL, nothing is filled in; in this case, addrlen is not used, and should also be NULL.——这是官方解释,翻译过来:参数 addr 是指向 sockaddr 结构的指针。此结构填充了对等套接字的地址,如通信层所知。这返回的地址 addr 的确切格式由套接字的地址族决定。当 addr 为 空指针 时,没有填写任何内容;在这种情况下,不使用 addrlen,也应为 空指针


addrlen:The addrlen argument is a value-result argument: the caller must initialize it to contain the size (in bytes) of the structure pointed to by addr; on return it will contain the actual size of the peer address.——这是官方解释,翻译过来:addrlen 参数是一个 value-result 参数:调用者必须对其进行初始化以包含 addr 指向的结构的大小(以字节为单位);返回时,它将包含对等地址的实际大小。


返回值

官方解释:On success, these system calls return a file descriptor for the accepted socket (a nonnegative integer). On error, -1 is returned, errno is set

​ to indicate the error, and addrlen is left unchanged.

翻译:成功后,这些系统调用将返回接受的套接字的文件描述符(非负整数)。出错时,返回 -1,errno 设置为指示错误,并且 addrlen 保持不变。


connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

官方解释:The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr.

翻译:connect() 系统调用将文件描述符 sockfd 引用的套接字连接到 addr 指定的地址。

理解助手: addr 这个指针指向了关于目标主机的 ip 地址和 port 端口号的地址空间,所以借助这个性质就可以将 sockfd 这个文件描述符对应的套接字跟目标主机的套接字进行连接。


sockfd:The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET.——这是官方解释,翻译过来:sockfd 参数是一个文件描述符,它引用 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字


addr:The argument addr is a pointer to a sockaddr structure. This structure is filled in with the address of the peer socket, as known to the communications layer. ——这是官方解释,翻译过来:参数 addr 是指向 sockaddr 结构的指针。此结构填充了对等套接字的地址,如通信层所知。


addrlen:The addrlen argument specifies the size of addr.—— 这是官方解释,翻译过来:addrlen 参数指定大小的地址。


返回值

官方解释:If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set to indicate the error.

翻译:如果连接或绑定成功,则返回 0。出错时,返回 -1,并设置 errno 以指示错误。


4、基于 socket api 实现一个十分十分简单的 UDP 服务器和客户端(重点)

4.1 客户端

头文件 UdpClient.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <strings.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUFFER_NUM 1024

enum {
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR,
    SENDTO_ERR,
    RECVFROM_ERR
};

class UdpClient
{
public:
    UdpClient(const std::string serverip, const uint16_t serverport):_serverip(serverip), _serverport(serverport), _quit(false)
    {}

    void init()
    {
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_socketfd == -1)
        {
            std::cerr << errno << ": "<< strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        //客户端不需要进行绑定
    }

    void run()
    {
        pthread_create(&reader, nullptr, read_message, &_socketfd);
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());
        server.sin_port = htons(_serverport);
        socklen_t serverlen = sizeof(server);
        std::string msg, rsp;
        char command_line[BUFFER_NUM];
        while(!_quit)
        {
            std::cerr << "Please Enter# ";
            fgets(command_line, sizeof(command_line)-1, stdin);//cin会自动将空格切割
            command_line[strlen(command_line)-1] = 0;
            msg = command_line;
            int n = sendto(_socketfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&server, sizeof(server));
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
            struct sockaddr_in temp;
        	socklen_t templen = sizeof(temp);
            n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &templen);
            //该函数会进行阻塞式等待消息
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(RECVFROM_ERR);
            }
            buffer[n] = 0;
            rsp = buffer;
            std::cout << rsp << std::endl;
        }
        close(_socketfd);
    }
    
    ~UdpClient()
    {}
private:
    std::string _serverip;//点分十进制
    uint16_t _serverport;//服务端的端口
    int _socketfd;//打开的文件描述符
    bool _quit;
};

主文件 UdpClient.cc

#include "UdpClient.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n[Usage]\n" << "\t" << proc << "\tserverip\t\tserverport\n" << std::endl;
    // std::cout << "\n[Usage]\n\t" << proc << "\tlocalport\n" << std::endl;
    exit(USAGE_ERR);
}

int main(int argc, char* argv[])
{
    if(argc < 3)
    {
        Usage(argv[0]);
    }


    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    std::unique_ptr<UdpClient> uc(new UdpClient(serverip, serverport));

    uc->init();
    uc->run();

    return 0;
}

4.2 服务端

头文件 UdpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cassert>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <sys/socket.h>

const int buffer_num = 1024;

std::string default_ip = "0.0.0.0";

enum {
    SOCKET_ERR = 1,
    BIND_ERR,
    USAGE_ERR,
    RECVFROM_ERR,
    OPEN_ERR,
    SENDTO_ERR
};

class UdpServer
{
public:
    using func_t = std::function<void(int, std::string, uint16_t, std::string)>;
public:
    UdpServer(func_t func, uint16_t serverport, std::string serverip = default_ip):_func(func), _serverport(serverport)
    {}
    void init()
    {
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_socketfd == -1)
        {
            std::cerr << errno << ": "<< strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        struct sockaddr_in local;
        bzero(&local, sizeof(local));

        local.sin_family = AF_INET;
        local.sin_port = htons(_serverport);
        local.sin_addr.s_addr = INADDR_ANY;
        //绑定的是服务器本地的端口,需要发送给网络,让客户端知道是哪个端口,因此这里需要用htons
        //就像快递单一样
        // si.sin_addr.s_addr = inet_addr(_serverip.c_str());
        // 不建议这样写,因为服务器可能不止有有一个ip,假如绑定了一个唯一的ip,那么发送给别的ip的数据将无法向上交付
        int n = bind(_socketfd, (struct sockaddr*)&local, sizeof(local));
        if(n == -1)
        {
            std::cerr << errno << ": "<< strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        (void)n;
    }
    void start()
    {
        for(;;)//服务端正常是死循环式进行运行的
        {
            char buffer[buffer_num];
            std::string message;
            struct sockaddr_in client;
            socklen_t clientlen = sizeof(client);
            int n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&client, &clientlen);
            //该函数会进行阻塞式等待消息
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(RECVFROM_ERR);
            }
            buffer[n] = 0;
            message = buffer;
            std::string clientip = inet_ntoa(client.sin_addr);
            uint16_t clientport = ntohs(client.sin_port);
            std::cout << clientip << "[" << clientport << "]# " << message << std::endl;
            _func(_socketfd, clientip, clientport, message);
        }
    }
    ~UdpServer()
    {}
private:
    std::string _serverip;//点分十进制
    uint16_t _serverport; //服务端的端口
    int _socketfd;		  //打开的文件描述符
    func_t _func;		  //对传入的数据进行处理的函数
};

makefile 展示:

all:UdpServer UdpClient

UdpServer:UdpServer.cc
	g++ -o $@ $^ -std=c++11
UdpClient:UdpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -rf UdpClient UdpServer

4.2.1 version 1:英文翻译的服务器

词典 dict.txt

hello:你好
apple:苹果
banana:香蕉
you:你

主文件 UdpServer.cc

#include "UdpServer.hpp"
#include <unordered_map>
#include <fstream>
#include <memory>
#include <cstdio>
#include <signal.h>

std::unordered_map<std::string, std::string> dict;//词典
const std::string mydict = "./dict.txt";//词典的位置

void Usage(std::string proc)//使用手册
{
    std::cout << "\n[Usage]\n\t" << proc << "\tlocalport\n" << std::endl;
    exit(USAGE_ERR);
}

bool cut_string(const std::string &line, std::string* key, std::string* value, const std::string &sep)//切割字符串
{
    size_t pos = line.find(sep);
    if(pos == std::string::npos) return false;//说明没有该分隔符
    *key = line.substr(0, pos);//[)
    *value = line.substr(pos + sep.size());//[)
    return true;
}

void dict_init()//初始化词典
{
    std::ifstream fin(mydict, std::ios::in);
    if(!fin.is_open())
    {
        std::cerr << errno << ": "<< strerror(errno) << std::endl;
        exit(OPEN_ERR);
    }
    std::string line;
    std::string key, value;
    while(getline(fin, line))
    {
        // std::cout << line << std::endl;
        if(cut_string(line, &key, &value, ":"))
        {
            dict.insert(std::make_pair(key, value));
        }
    }
    fin.close();
    std::cout << "dict init success!" << std::endl;
}

void debug_dict_init()//用于测试词典是否初始化成功
{
    for(auto &it: dict)
    {
        std::cout << it.first << "# " << it.second << std::endl;
    }
}

void update_dict(int signum)//用于热加载词典:无需将服务器关闭就能够对词典进行更新
{
    dict_init();
    std::cout << "reload dict success" << std::endl;
}

void translate(int socketfd, std::string clientip, uint16_t clientport, std::string message)//服务器对客户端发来的单词进行翻译
{
    auto it = dict.find(message);
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);
    std::string response;
    if(it == dict.end()) response = "unknown";
    else response = it->second;
    int n = sendto(socketfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof(client));
    if(n == -1)
    {
        std::cerr << errno << ": "<< strerror(errno) << std::endl;
        exit(SENDTO_ERR);
    }    
}

int main(int argc, char* argv[])
{
    signal(2, update_dict);
    //可以对词典进行热加载:在不停止服务器的情况下,对词典进行更新,eg:游戏不停服更新

    if(argc < 2)
    {
        Usage(argv[0]);
    }

    dict_init();

    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> us(new UdpServer(translate, port));

    us->init();
    us->start();
    return 0;
}

运行流程展示

服务端

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ make
g++ -o UdpServer UdpServer.cc -std=c++11
g++ -o UdpClient UdpClient.cc -std=c++11
wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ ./UdpServer 8080
dict init success!
127.0.0.1[42016]# hello
127.0.0.1[42016]# nishi

客户端

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ ./UdpClient 127.0.0.1 8080
Please Enter# hello
你好
Please Enter# nishi
unknown

4.2.2 version 2:执行命令解释的服务器

主文件 UdpServer.cc

#include "UdpServer.hpp"
#include <unordered_map>
#include <fstream>
#include <memory>
#include <cstdio>
#include <signal.h>

void command_exec(int socketfd, std::string clientip, uint16_t clientport, std::string command)
{
    char buffer[buffer_num];
    std::string response;
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);
    FILE* command_ret = popen(command.c_str(), "r");
    if(command_ret == nullptr) response = "open err";
    else
    {
        while(fgets(buffer, sizeof(buffer)-1, command_ret))
        {
            response += buffer;
        }
    }
    int n = sendto(socketfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof(client));
    if(n == -1)
    {
        std::cerr << errno << ": "<< strerror(errno) << std::endl;
        exit(SENDTO_ERR);
    }
    pclose(command_ret);
}

int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        Usage(argv[0]);
    }

    uint16_t port = atoi(argv[1]);

    std::unique_ptr<UdpServer> us(new UdpServer(command_exec, port));

    us->init();
    us->start();
    return 0;
}

运行流程展示

服务端

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ make
g++ -o UdpServer UdpServer.cc -std=c++11
g++ -o UdpClient UdpClient.cc -std=c++11
wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ ./UdpServer 8080
127.0.0.1[49663]# ls
127.0.0.1[49663]# ls -l

客户端

./UdpClient 127.0.0.1 8080
Please Enter# ls
dict.txt
makefile
OnlineClient.hpp
UdpClient
UdpClient.cc
UdpClient.hpp
UdpServer
UdpServer.cc
UdpServer.hpp

Please Enter# ls -l
total 196
-rw-rw-r-- 1 wjy wjy     47 Mar 26 20:16 dict.txt
-rw-rw-r-- 1 wjy wjy    169 Jun 17 20:01 makefile
-rw-rw-r-- 1 wjy wjy   4318 Mar 26 20:16 OnlineClient.hpp
-rwxrwxr-x 1 wjy wjy  30920 Jun 17 20:12 UdpClient
-rw-rw-r-- 1 wjy wjy    557 Mar 26 20:16 UdpClient.cc
-rw-rw-r-- 1 wjy wjy   2909 Mar 26 20:16 UdpClient.hpp
-rwxrwxr-x 1 wjy wjy 129568 Jun 17 20:12 UdpServer
-rw-rw-r-- 1 wjy wjy   4385 Jun 17 20:08 UdpServer.cc
-rw-rw-r-- 1 wjy wjy   2993 Mar 26 20:16 UdpServer.hpp

Please Enter# 

4.2.3 version 3:聊天室版的服务器

在聊天室版中,为什么要对客户端的读写用多进程的方式来完成?

这涉及到我们日常使用,我们在用 QQ,微信 的时候,聊天内容与输入框是分离的,所以,为了达成这个目的,这里使用了多进程的方式,将读端放到孙子进程中,然后通过管道的方式来将数据取出放到显示器上。

头文件 OnlineClient.hpp

#pragma once

#include "UdpServer.hpp"
#include <unordered_map>

class User
{
public:
    User(std::string user_ip, uint16_t user_port):_user_ip(user_ip), _user_port(user_port)
    {}
    ~User()
    {}
    std::string get_user_ip()
    {
        return _user_ip;
    }
    uint16_t get_user_port()
    {
        return _user_port;
    }
private:
    std::string _user_ip;
    uint16_t _user_port;
};

class OnlineClient
{
public:
    OnlineClient()
    {}
    bool is_online(int socketfd, std::string user_ip, uint16_t user_port)//判断该用户是否已经上线了
    {
        std::string response;
        std::string _user_port = std::to_string(user_port);
        std::string id = user_ip + "-" + _user_port;
        auto it = online_client.find(id);
        if(it == online_client.end()) 
        {
            response = "please online at first! you should enter \"online\" to make it!";
            struct sockaddr_in client;
            bzero(&client, sizeof(client));

            client.sin_family = AF_INET;
            client.sin_port = htons(user_port);
            client.sin_addr.s_addr = inet_addr(user_ip.c_str());
            int n = sendto(socketfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof(client));
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
            return false;
        }
        return true;
    }
    void add_user(int socketfd, std::string user_ip, uint16_t user_port)//增加一名新用户
    {
        std::string response;

        std::string _user_port = std::to_string(user_port);
        std::string id = user_ip + "-" + _user_port;
        User new_user(user_ip, user_port);
        online_client.insert(std::make_pair(id, new_user));

        response = "online success!";
        struct sockaddr_in client;
        bzero(&client, sizeof(client));

        client.sin_family = AF_INET;
        client.sin_port = htons(user_port);
        client.sin_addr.s_addr = inet_addr(user_ip.c_str());
        int n = sendto(socketfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof(client));
        if(n == -1)
        {
            std::cerr << errno << ": "<< strerror(errno) << std::endl;
            exit(SENDTO_ERR);
        }
    }
    void del_user(int socketfd, std::string user_ip, uint16_t user_port)//删除用户
    {
        if(is_online(socketfd, user_ip, user_port))
        {
            std::string response;

            std::string _user_port = std::to_string(user_port);
            std::string id = user_ip + "-" + _user_port;
            online_client.erase(id);
            
            response = "del success!";
            struct sockaddr_in client;
            bzero(&client, sizeof(client));

            client.sin_family = AF_INET;
            client.sin_port = htons(user_port);
            client.sin_addr.s_addr = inet_addr(user_ip.c_str());
            int n = sendto(socketfd, response.c_str(), response.size(), 0, (struct sockaddr*)&client, sizeof(client));
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
        }
    }
    void broadcast_message(int socketfd, std::string user_ip, uint16_t user_port, const std::string& message)
    //将消息传给该聊天室的每一个在线用户
    {   
        std::string _user_port = std::to_string(user_port);
        std::string id = user_ip + "-" + _user_port;
        for(auto &it: online_client)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));

            client.sin_family = AF_INET;
            client.sin_port = htons(it.second.get_user_port());
            client.sin_addr.s_addr = inet_addr(it.second.get_user_ip().c_str());

            std::string msg = id + "# " + message;

            int n = sendto(socketfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&client, sizeof(client));
            if(n == -1)
            {
                std::cerr << errno << ": "<< strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
        }
    }
    ~OnlineClient()
    {}
private:
    std::unordered_map<std::string, User> online_client;
};

主文件 UdpServer.cc

#include "UdpServer.hpp"
#include "OnlineClient.hpp"
#include <unordered_map>
#include <fstream>
#include <memory>
#include <cstdio>
#include <signal.h>

OnlineClient onlineClient;

void route_message(int socketfd, std::string clientip, uint16_t clientport, std::string message)
{
    std::string response;
    if(message == "online") { onlineClient.add_user(socketfd, clientip, clientport); return;}
    if(!onlineClient.is_online(socketfd, clientip, clientport))
    {
        // std::cout << "please online at first! you should enter \"online\" to make it!" << std::endl;
        return;
    }
    if(message == "offline") { onlineClient.del_user(socketfd, clientip, clientport); return;}
    onlineClient.broadcast_message(socketfd, clientip, clientport, message);
}

int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        Usage(argv[0]);
    }
    
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> us(new UdpServer(route_message, port));
    us->init();
    us->start();
    return 0;
}

运行流程展示

服务端

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ make
g++ -o UdpServer UdpServer.cc -std=c++11
g++ -o UdpClient UdpClient.cc -std=c++11
wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ ./UdpServer 8080
127.0.0.1[59095]# hello
127.0.0.1[59095]# online
127.0.0.1[59095]# hello
127.0.0.1[59095]# offline
127.0.0.1[59095]# hello

客户端

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ ./UdpClient 127.0.0.1 8080 > fifo1
Please Enter# hello
Please Enter# online
Please Enter# hello
Please Enter# offline
Please Enter# hello
Please Enter# 

fifo1

wjy@VM-4-8-ubuntu:~/myrepo/learning_git/project/Udp$ cat < fifo1
please online at first! you should enter "online" to make it!
online success!
127.0.0.1-47630# hello
del success!
please online at first! you should enter "online" to make it!

5、基于 socket api 实现一个十分简单的 TCP 服务器

在实现这份代码之前,我们先进行一项准备工作——完成 日志函数

5.1 准备工作

日志文件 logMessage.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <ctime>
#include <stdarg.h>
#include <fcntl.h>

#define LOGNUM 1024

#define DEBUG      0
#define NORMAL     1
#define WARNING    2
#define ERROR      3
#define FATAL      4

#define PATH "./log.txt"

std::string msg(int level)
{
    switch(level)
    {
        case DEBUG: return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default: return nullptr;
    }
}

void logMessage(int log_level, const char* format, ...)
{
    FILE* f = fopen(PATH, "a");
    //[等级][时间][消息]
    std::string logprefix;
    logprefix = "[" + msg(log_level) + "]" + "[" + std::to_string(time(nullptr)) + "]";

    va_list args;
    va_start(args, format);
    char logline[LOGNUM];
    
    vsnprintf(logline, sizeof(logline), format, args);

    std::string log = logprefix + logline;
    std::cout << logprefix << logline << std::endl;
    fprintf(f, "%s\n",log.c_str());
    fclose(f);
}

makefile 展示:

all:TcpClient TcpServer

TcpClient:TcpClient.cc
	g++ -o $@ $^ -std=c++11
TcpServer:TcpServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f TcpClient TcpServer

5.2 客户端

头文件 TcpClient.hpp

#pragma once 

#include <iostream>
#include <string>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

#define BUF_NUM 1024

namespace tcpc
{
    class TcpClient
    {
    public:
        TcpClient(const std::string &serverip, const uint16_t &serverport)
        :_sock(-1), _serverip(serverip), _serverport(serverport)
        {}
        void initTcpClient()
        {
            //step 1:创建客户端自身的套接字
            _sock = socket(AF_INET, SOCK_STREAM, 0);
            if(_sock < 0)
            {
                std::cout << "socket create error" << std::endl;
                exit(1);
            }
            std::cout << "socket create success" << std::endl;
            //不需要显示的bind,让 OS 自动帮我们去选择相应的ip和接口
        }
        void runTcpClient()
        {
            struct sockaddr_in server;
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);
            //step 2:将客户端的套接字与服务端套接字进行链接
            if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
            {
                std::cout << "socket connect error" << std::endl;
                exit(2);
            }
            else
            {
                std::cout << "socket connect success" << std::endl;
                std::string msg;
                while(true)
                {   
                    std::cout << "Please Enter# ";
                    std::getline(std::cin, msg);
                    write(_sock, msg.c_str(), msg.size());
                    char buffer[BUF_NUM];
                    int n = read(_sock, buffer, sizeof(buffer)-1);
                    if(n > 0)
                    {
                        buffer[n] = 0;
                        std::cout << "服务器回应# " << buffer << std::endl;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
        ~TcpClient()
        {
            close(_sock);
        }
    private:
        std::string _serverip;
        uint16_t _serverport;
        int _sock;//客户端套接字,用于与服务端进行链接
    };
}

主文件 TcpClient.cc

#include "TcpClient.hpp"
#include <memory>

//./TcpClient 127.0.0.1 8080
void Usage(std::string proc)
{
    std::cout << "\nUsage:\n\t" << proc << "\tserverip\tserverport\n" <<std::endl;
    exit(1);
}

int main(int argc, char* argv[])
{
    if(argc < 3)
    {
        Usage(argv[0]);
    }

    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    std::unique_ptr<tcpc::TcpClient> tcpclt(new tcpc::TcpClient(serverip, serverport));
    tcpclt->initTcpClient();
    tcpclt->runTcpClient();
    return 0;
}

5.3 服务端

主文件 TcpServer.cc

#include "TcpServer.hpp"
#include "daemon.hpp"
#include <memory>

using tcps::USAGE;

void Usage(std::string proc)
{
    std::cout << "\nUsage:\n\t" << proc << "\tlocalport\n" <<std::endl;
    exit(USAGE);
}

// ./TcpServer localport
int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        Usage(argv[0]);
    }

    uint16_t serverport = atoi(argv[1]);
    std::unique_ptr<tcps::TcpServer> tcpsvr(new tcps::TcpServer(serverport));
    tcpsvr->initTcpServer();
    daemonslf();
    tcpsvr->runTcpServer();

    return 0;
}

5.3.1 version 1:单进程版的服务器(最基础)

头文件 TcpServer.hpp

#pragma once 

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "logMessage.hpp"

#define BUF_NUM 1024

namespace tcps
{
    enum
    {
        USAGE = 1,
        SOCK_ERR,
        BIND_ERR,
        LISTEN_ERR  
    };

    class TcpServer;

    const std::string gip = "0.0.0.0";

    class TcpServer
    {
    public:
        TcpServer(const uint16_t & serverport)
        :_listensockfd(-1), _serverport(serverport)
        {}
        void initTcpServer()
        {
            //step 1:构建一个服务器监听套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(_listensockfd < 0)
            {
                logMessage(FATAL, "socket create error");
                exit(SOCK_ERR);
            }
            logMessage(NORMAL, "socket create success");
            //step 2:将服务器监听套接字与对应的端口进行绑定
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_serverport);
            local.sin_addr.s_addr = INADDR_ANY;
            
            if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
            {
                logMessage(FATAL, "socket bind error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "socket bind success");

            //step 3:开始监听
            if(listen(_listensockfd, 4) != 0)
            {
                logMessage(FATAL, "socket listen error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "socket listen success");
        }
        void runTcpServer()
        {
            //step 4:开始与客户端套接字进行链接
            for(;;)
            {
                struct sockaddr_in client;
                socklen_t clientlen = sizeof(client);
                int sock = accept(_listensockfd, (struct sockaddr*)&client, &clientlen);
                if(sock < 0) 
                {
                    logMessage(ERROR, "socket accept error");
                    continue;
                }
                logMessage(NORMAL, "socket accept success: %d", sock);
                
                //version 1:single process
                //接受客户端的信息
                serviceIO(sock, client);
                //最后要把这个刚建的套接字关掉,否则可能会有套接字泄露问题
                close(sock);
            }
        }
        void serviceIO(int sock)
        {
            char buffer[BUF_NUM];
            while(true)
            {
                std::string msg;
                int n = read(sock, buffer, sizeof(buffer)-1);
                msg = buffer;
                if(n > 0) std::cout << msg << std::endl;
                else if(n == 0) break;//说明客户端退出了
                //服务端将客户端信息进行回显
                msg += " server[echo]";
                write(sock, msg.c_str(), msg.size());
            }
        }
        ~TcpServer()
        {
            close(_listensockfd);
        }
    private:
        std::string _serverip;
        uint16_t _serverport;
        int _listensockfd;//用于监听的文件描述符//监听的文件描述符相当于拉客的服务员,只需要一个就可以了
    };
}

5.3.2 version 2:多进程版的服务器(ez)

头文件 TcpServer.hpp

#pragma once 

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include "logMessage.hpp"

#define BUF_NUM 1024

namespace tcps
{
    enum
    {
        USAGE = 1,
        SOCK_ERR,
        BIND_ERR,
        LISTEN_ERR  
    };

    class TcpServer;

    const std::string gip = "0.0.0.0";

    class TcpServer
    {
    public:
        TcpServer(const uint16_t & serverport)
        :_listensockfd(-1), _serverport(serverport)
        {}
        void initTcpServer()
        {
            //step 1:构建一个服务器监听套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(_listensockfd < 0)
            {
                logMessage(FATAL, "socket create error");
                exit(SOCK_ERR);
            }
            logMessage(NORMAL, "socket create success");
            //step 2:将服务器监听套接字与对应的端口进行绑定
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_serverport);
            local.sin_addr.s_addr = INADDR_ANY;
            
            if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
            {
                logMessage(FATAL, "socket bind error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "socket bind success");

            //step 3:开始监听
            if(listen(_listensockfd, 4) != 0)
            {
                logMessage(FATAL, "socket listen error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "socket listen success");
        }
        void runTcpServer()
        {
            //version 2.2:mul process by signal
            signal(SIGCHLD, SIG_IGN);
            //step 4:开始与客户端套接字进行链接
            for(;;)
            {
                struct sockaddr_in client;
                socklen_t clientlen = sizeof(client);
                int sock = accept(_listensockfd, (struct sockaddr*)&client, &clientlen);
                if(sock < 0) 
                {
                    logMessage(ERROR, "socket accept error");
                    continue;
                }
                logMessage(NORMAL, "socket accept success: %d", sock);

                pid_t id = fork();
                if(id == 0)//child
                {
                    //version 2.1:mul process by grandson process
                    // if(fork()>0) exit(0);//此时创建一个孙子进程,让这个孙子进程成为一个孤儿进程,由操作系统来回收对应的资源
                    // 通过忽略该信号可以使该进程不需要由父进程来回收,最后交给 OS 来完成回收,而且多次创建进程对 OS 开销过大
                    serviceIO(sock);
                    close(sock);
                }
            }
        }
		void serviceIO(int sock)
        {
            char buffer[BUF_NUM];
            while(true)
            {
                std::string msg;
                int n = read(sock, buffer, sizeof(buffer)-1);
                msg = buffer;
                if(n > 0) std::cout << msg << std::endl;
                else if(n == 0) break;//说明客户端退出了
                //服务端将客户端信息进行回显
                msg += " server[echo]";
                write(sock, msg.c_str(), msg.size());
            }
        }
        ~TcpServer()
        {
            close(_listensockfd);
        }
    private:
        std::string _serverip;
        uint16_t _serverport;
        int _listensockfd;//用于监听的文件描述符//监听的文件描述符相当于拉客的服务员,只需要一个就可以了
    };
}

这一版给了两种方案:

​ 1、用该进程创建一个子进程,再用这个子进程创建一个孙子进程,再将这个子进程关闭,这样孙子进程就会变成一个孤儿进程,后续的回收工作就由 OS 来完成了——这种方案构思巧妙,但是这种方案并不是一种好的方案,因为多次创建进程对 OS 开销过大。

​ 2、对 SIGCHLD 信号进行忽略,后续父进程也不需要再知道子进程的情况,后续的回收也是通过 OS 来完成的——这种方案简单,并且不会给 OS 带来巨大的开销。


5.3.3 version 3:多线程版的服务器(正常)

头文件 TcpServer.hpp

#pragma once 

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include "logMessage.hpp"

#define BUF_NUM 1024

namespace tcps
{
    enum
    {
        USAGE = 1,
        SOCK_ERR,
        BIND_ERR,
        LISTEN_ERR  
    };

    class TcpServer;

    const std::string gip = "0.0.0.0";

    class ThreadData
    {
    public:
        ThreadData(TcpServer* td, int sock)
        :_this(td), _sock(sock)
        {}
    public:
        TcpServer* _this;
        int _sock;
    };

    class TcpServer
    {
    public:
        TcpServer(const uint16_t & serverport)
        :_listensockfd(-1), _serverport(serverport)
        {}
        void initTcpServer()
        {
            //step 1:构建一个服务器监听套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(_listensockfd < 0)
            {
                logMessage(FATAL, "socket create error");
                exit(SOCK_ERR);
            }
            logMessage(NORMAL, "socket create success");
            //step 2:将服务器监听套接字与对应的端口进行绑定
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_serverport);
            local.sin_addr.s_addr = INADDR_ANY;
            
            if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
            {
                logMessage(FATAL, "socket bind error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "socket bind success");

            //step 3:开始监听
            if(listen(_listensockfd, 4) != 0)
            {
                logMessage(FATAL, "socket listen error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "socket listen success");
        }
        void runTcpServer()
        {
            //step 4:开始与客户端套接字进行链接
            for(;;)
            {
                struct sockaddr_in client;
                socklen_t clientlen = sizeof(client);
                int sock = accept(_listensockfd, (struct sockaddr*)&client, &clientlen);
                if(sock < 0) 
                {
                    logMessage(ERROR, "socket accept error");
                    continue;
                }
                logMessage(NORMAL, "socket accept success: %d", sock);

                //version 3:mul thread
                pthread_t pid;
                ThreadData* td = new ThreadData(this, sock);
                pthread_create(&pid, nullptr, handler, td);

            }
        }
        static void* handler(void* args)
        {
            //由OS回收这部分资源
            pthread_detach(pthread_self());
            ThreadData* td = static_cast<ThreadData*>(args);
            td->_this->serviceIO(td->_sock);
            close(td->_sock);
            delete td;
            return nullptr;
        }
        void serviceIO(int sock)
        {
            char buffer[BUF_NUM];
            while(true)
            {
                std::string msg;
                int n = read(sock, buffer, sizeof(buffer)-1);
                msg = buffer;
                if(n > 0) std::cout << msg << std::endl;
                else if(n == 0) break;//说明客户端退出了
                //服务端将客户端信息进行回显
                msg += " server[echo]";
                write(sock, msg.c_str(), msg.size());
            }
        }
        ~TcpServer()
        {
            close(_listensockfd);
        }
    private:
        std::string _serverip;
        uint16_t _serverport;
        int _listensockfd;//用于监听的文件描述符//监听的文件描述符相当于拉客的服务员,只需要一个就可以了
    };
}

5.3.4 version 4:线程池版的服务器(困难)

自行封装的互斥锁头文件 Mutex.hpp

#pragma once

#include <iostream>
#include <pthread.h>

//RAII性质的互斥量
class Mutex
{
public:
    Mutex(pthread_mutex_t* mutex = nullptr):mutex_(mutex) { }
    void lock()
    {
        if(mutex_) pthread_mutex_lock(mutex_);
    }
    void unlock()
    {
        if(mutex_) pthread_mutex_unlock(mutex_);
    }
    ~Mutex()
    {
        //nothing
    }
private:
    pthread_mutex_t* mutex_;
};

class LockGaurd
{
public:
    LockGaurd(pthread_mutex_t* mtx):mutex_(mtx)
    {
        mutex_.lock();
    }
    ~LockGaurd()
    {
        mutex_.unlock();
    }
private:
    Mutex mutex_;
};

自行封装的线程文件 Thread.hpp

#pragma once

#include <iostream>
#include <functional>
#include <string>
#include <cassert>
#include <cstdio>
#include <pthread.h>

#define NAME_NUM 1024

class Thread;//先声明这个类

template <class T>
class Context/*用于包装线程的信息*/
{
public:
    Context():this_(nullptr), args_(nullptr){ }
    Context(T* _this, void* args = nullptr):this_(_this), args_(args){ }
public:
    T* this_;
    void* args_;//参数
};

class Thread/*封装一个自己的线程库*/
{
public:
    using func_t = std::function<void*(void*)>;//重命名函数类型

public:
    Thread()
    {
        char namebuffer[NAME_NUM];
        snprintf(namebuffer, sizeof namebuffer, "thread-> %d", num_++);
        name_ = namebuffer;
    }

    void start(func_t func, void* args = nullptr)
    {
        func_ = func;
        args_ = args;
        Context<Thread>* ctx = new Context<Thread>();
        ctx->this_ = this;
        ctx->args_ = args_;
        int n = pthread_create(&tid_, nullptr, start_routine, ctx);
        assert(n == 0);
        (void)n;
    }
 private:
    static void* start_routine(void* args)
    //默认自带了一个this指针,所以该函数的类型为 void*(*)(void*,this):不是void*(*)(void*),不符合要求
    {
        Context<Thread>* ctx = static_cast<Context<Thread>*>(args);
        void * ret = ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
    }

    void* run(void* args)//运行函数,用于包装func_
    {
        return func_(args);
    }

public:   
    int join()//线程等待
    {
        int n = pthread_join(tid_, nullptr);
        assert(n == 0);
        return n;
    }
    
    pthread_t gettid()
    {
        return tid_;
    }

    std::string getname()
    {
        return name_;
    }

    ~Thread()
    {
        //do nothing
    }

private:
    pthread_t tid_;//线程id
    std::string name_;//线程名字
    static int num_;//线程编号
    void* args_;//参数
    func_t func_;//子线程进行的操作
};

int Thread::num_ = 1;

线程池文件 threadpool.hpp

#pragma once

#include "Thread.hpp"
#include "Mutex.hpp"
#include "logMessage.hpp"
#include <queue>//盛放任务的载体
#include <vector>//用于盛放线程的载体
#include <mutex>

static const int gtdnum = 5;

template<class T>
class ThreadPool;

template<class T>
class ThreadData
{
public:
    ThreadData(ThreadPool<T>* this_, std::string name_)
    :_this(this_), _name(name_)
    {}
public:
    ThreadPool<T>* _this;
    std::string _name;
};

template<class T>
class ThreadPool
{
private:
    static void* handler(void* args)//静态成员函数无法访问动态成员变量,所以需要另一个类来保存
    {
        ThreadData<T>* td = static_cast<ThreadData<T>*> (args);
        while(true)
        {
            td->_this->queue_lock();
            while(td->_this->task_empty()) 
            {
                td->_this->thread_wait();
            }
            T t = td->_this->pop();
            td->_this->queue_unlock();
            t();
        }
        delete td;
        return nullptr;
    }
private:
    void queue_lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void queue_unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }
    void thread_wait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    bool task_empty()
    {
        return _task_queue.empty();
    }
    T pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
private:
    ThreadPool(int thread_num = gtdnum):_thread_num(thread_num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for(int i = 0; i < _thread_num; i++)
        {
            _thread_pool.push_back(new Thread());
        }
    }
public:
    void run()
    {
        for(auto &it :_thread_pool)
        {
            ThreadData<T>* td = new ThreadData<T>(this, it->getname());

            logMessage(NORMAL, "%s run......", it->getname().c_str());
            it->start(handler, td);
        }
    }
    void push(const T& task)
    {
        LockGaurd lkg(&_mutex);
        pthread_cond_signal(&_cond);
        _task_queue.push(task);
    }
    
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for(auto &it :_thread_pool)
        {
            delete it;
        }
    }

    ThreadPool(const ThreadPool<T>&) = delete;//拷贝构造不允许
    void operator =(const ThreadPool<T> & td) = delete;//在单例模式中,赋值操作不允许
public:
    static ThreadPool<T>* getInstance()
    {
        if(tp == nullptr)
        {
            std::mutex mtx;
            mtx.lock();
            if(tp == nullptr)
            {
                tp = new ThreadPool<T>();
            }
            mtx.unlock();
        }
        return tp;
    }
private:
    std::vector<Thread*> _thread_pool;//线程池
    std::queue<T> _task_queue;//任务队列
    pthread_mutex_t _mutex;//互斥锁
    pthread_cond_t _cond;//条件变量
    int _thread_num;//线程最大数量
private:
    static ThreadPool<T>* tp;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::tp = nullptr;

任务文件 Task.hpp

#pragma once

#include <iostream>
#include <functional>
#include <cstdio>

#define NUM 1024

void serviceIO(int sock)
{
    char buffer[NUM];
    while(true)
    {
        std::string msg;
        int n = read(sock, buffer, sizeof(buffer)-1);
        if(n > 0) //std::cout << clientip << "[" << clientport << "]" << msg << std::endl;
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
        else if(n == 0) break;//说明客户端退出了
        //服务端将客户端信息进行回显
        msg = buffer;
        msg += " server[echo]";
        write(sock, msg.c_str(), msg.size());
    }
}

class Task
{
private:
    using func_t = std::function<void(int)>;
public:
    Task(){}
    Task(int sock, func_t func)
    :_sock(sock), _func(func)
    {}
    void operator ()()
    {
        _func(_sock);
    } 
private:
    func_t _func;
    int _sock;
};

头文件 TcpServer.hpp

#pragma once 

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "logMessage.hpp"
#include "threadpool.hpp"
#include "Task.hpp"

#define BUF_NUM 1024

namespace tcps
{
    enum
    {
        USAGE = 1,
        SOCK_ERR,
        BIND_ERR,
        LISTEN_ERR  
    };

    class TcpServer;

    const std::string gip = "0.0.0.0";

    class ThreadData
    {
    public:
        ThreadData(TcpServer* td, int sock)
        :_this(td), _sock(sock)
        {}
    public:
        TcpServer* _this;
        int _sock;
    };

    class TcpServer
    {
    public:
        TcpServer(const uint16_t & serverport /*, const std::string &serverip = gip*/)
        :_listensockfd(-1), _serverport(serverport) /*, _serverip(serverip)*/
        {}
        void initTcpServer()
        {
            //step 1:构建一个服务器监听套接字
            _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(_listensockfd < 0)
            {
                logMessage(FATAL, "socket create error");
                exit(SOCK_ERR);
            }
            logMessage(NORMAL, "socket create success");
            //step 2:将服务器监听套接字与对应的端口进行绑定
            struct sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_serverport);
            local.sin_addr.s_addr = INADDR_ANY;
            
            if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
            {
                logMessage(FATAL, "socket bind error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "socket bind success");

            //step 3:开始监听
            if(listen(_listensockfd, 4) != 0)
            {
                logMessage(FATAL, "socket listen error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "socket listen success");
        }
        void runTcpServer()
        {
            //step 4:开始与客户端套接字进行链接
            for(;;)
            {
                struct sockaddr_in client;
                socklen_t clientlen = sizeof(client);
                int sock = accept(_listensockfd, (struct sockaddr*)&client, &clientlen);
                if(sock < 0) 
                {
                    logMessage(ERROR, "socket accept error");
                    continue;
                }
                logMessage(NORMAL, "socket accept success: %d", sock);
                
	            //version 4:threadpool
                Task t(sock, serviceIO);
                ThreadPool<Task>::getInstance()->run();
                ThreadPool<Task>::getInstance()->push(t);
            }
        }
        ~TcpServer()
        {
            close(_listensockfd);
        }
    private:
        std::string _serverip;
        uint16_t _serverport;
        int _listensockfd;//用于监听的文件描述符//监听的文件描述符相当于拉客的服务员,只需要一个就可以了
    };
}

EXTRA 5.3.5 version 5:守护进程化的服务器

知识助手:

​ 我们正常执行一个文件的时候,假如我们将这个会话关闭,这个执行文件的进程也会随着消失。而守护进程就是将一个进程与会话之间断开联系,从而达到即使会话被关闭,这个进程也不会因此被关闭。

想要实现一个守护进程,我们就需要知道实现守护进程有哪些条件

条件一,这个进程不能是该进程组的组长。

知识助手:

   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND

​ 这上面的 PGID 就是指的进程组,而与该进程组编号相同的进程就是该进程组的组长。

条件二,这个进程需要独立会话,不能与 bash 处于同一个会话。

知识助手:

​ Linux 中,有一句话:前台只有一个,后台可以有无数个。大概指的就是这种状况。

条件三,使用 setsid() 这个系统调用。

知识助手:

​ 下面介绍一下 setsid() 这个系统调用。

#include <unistd.h>

pid_t setsid(void);

官方描述:setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new session. The calling process also becomes the process group leader of a new process group in the session.

翻译:如果调用进程不是进程组领导,则 setsId() 会创建一个新会话。调用进程是新会话的领导者。调用进程还成为会话中新进程组的进程组领导。


daemon.hpp 实现如下:

#pragma once

#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>

void daemonslf(const char* pathname = nullptr)
{
    //step 1:先将进程变成孤儿进程
    pid_t id = fork();
    if(id>0) exit(0);

    //step 2:将该孤儿进程设置为守护进程
    setsid();

    //step 3:将不要的文件描述符关闭
    int sock = open("/dev/null", O_RDWR);
    if(sock >= 0)
    {
        dup2(sock, 0);
        dup2(sock, 1);
        dup2(sock, 2);
    }
    else
    {
        close(0);
        close(1);
        close(2);
    }

    //step 4:可选项
    if(pathname) chdir(pathname);
}

知识助手:

​ Linux 下,"/dev/null" 这个文件相当于黑洞,无论什么内容通过 cin 这个流输入到这个文件中,最后都会被丢弃,假如想通过 coutcerr 从这个文件中获取数据,最终也不会获取到任何数据。借此性质,可以将不必要输入输出都关闭掉,而且不会带来代码中无意中将数据输出到屏幕上而带来的错误。

修改后的主文件 TcpServer.cc

#include "TcpServer.hpp"
#include "daemon.hpp"
#include <memory>

using tcps::USAGE;

void Usage(std::string proc)
{
    std::cout << "\nUsage:\n\t" << proc << "\tlocalport\n" <<std::endl;
    exit(USAGE);
}

// ./TcpServer localport
int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        Usage(argv[0]);
    }

    uint16_t serverport = atoi(argv[1]);
    std::unique_ptr<tcps::TcpServer> tcpsvr(new tcps::TcpServer(serverport));
    tcpsvr->initTcpServer();
    daemonslf();
    tcpsvr->runTcpServer();

    return 0;
}

6、TCP 协议通信原理

通常 TCP 协议通信如下图所示:

屏幕截图 2025-06-18 033930.png

6.1 三次握手

第一次握手:客户端先向服务器发起请求连接。——客户端状态:SYN_SENT;服务器状态:SYN_RCVD

第二次握手:服务器收到请求后,也向客户端发起连接请求,并进行了确认应答。——客户端状态:ESTABLISHED;服务器状态:SYN_RCVD

第三次握手:客户端收到了服务器发来的确认应答后,也向服务器进行了确认应答。——客户端状态:ESTABLISHED;服务器状态:ESTABLISHED

知识助手:

​ 1、ACK 这是一个标记位,是确认应答机制中的重要的一节。

​ 2、第二次握手其实可以被拆成两次握手过程,但是由于这两环节通常是连在一起的,所以被整合到同一次握手中了。

6.2 四次握手

第一次挥手:由 A 方向 B 方发送断开连接请求。——A 方状态:FIN_WAIT_1;B 方状态:CLOSE_WAIT

第二次挥手:B 方收到 A 方的断开请求后,B 方向 A 方进行确认应答。——A 方状态:FIN_WAIT_2;B 方状态:CLOSE_WAIT

第三次挥手:一段时间后,B 方向 A 方发送断开连接请求。——A 方状态:TIME_WAIT;B 方状态:LAST_ACK

第四次挥手:A 方收到 B 方的断开请求后,A 方向 B 方进行确认应答。——A 方状态:TIME_WAIT;B 方状态:CLOSED