网络基础概念和套接字编程(Linux)

网络基础概念和套接字编程

前言

发展和网络协议

  1. 发展:从独立模式(计算机之间相互独立)到网络互联,再到局域网(LAN),再到现在的广域网(WAN)。简而言之就是通过各种设备扩大互联的范围。
    • 网络互联:多台计算机连接在一起, 完成数据共享
    • 局域网 (LAN):通过交换机和路由器连接在一起,范围小。主流的局域网包括以太网和无线LAN
    • 广域网 (WAN):通过路由器连接,把相隔千里的计算机连在一起(较大的局域网)
  2. 协议:就是一种约定。
    网络协议:约定一个共同的标准。因为需要一个共同标准约束不同的厂商生产的计算机、硬件和装载的不同系统适配网络。

一、网络协议

1. 协议分层

  • 无论是OSI七层模型和TCP/IP四层(或五层)模型,都进行了分层,每层负责完成不同功能。接下来对两个模型都进行简要介绍,主要介绍TCP/IP四层模型。
  • 分层的原因:最大好处是"封装",每一层只关心自己的事,底层的变化不影响上层,同时也解耦

①OSI七层模型(理论化)

OSI(Open System Interconnection,开放系统互连)七层网络模型是一个逻辑上的定义和规范,是理论化没有实际化。

  • 每一层都有相关、相对应的物理设备,比如路由器,交换机
  • 最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整。通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯
层级 分层名称 功能 每层功能概要(及示例)
7 应用层 针对特定应用的协议。 针对每个应用的协议 • 电子邮件 → 电子邮件协议 • 远程登录 → 远程登录协议 • 文件传输 → 文件传输协议
6 表示层 设备固有数据格式和网络标准数据格式的转换。 网络标准格式 接收不同表现形式的信息,如文字、图像、声音等。
5 会话层 通信管理。负责建立和断开通信连接(数据流动的逻辑通路),以及数据的分割等数据传输相关的管理。 何时建立连接,何时断开连接以及保持多久的连接
4 传输层 管理两个"节点"之间的数据传输。负责可靠传输(确保数据被可靠地传送到目标地址)。 是否有数据丢失
3 网络层 地址管理与路由选择。 经过哪个路由传送到目标地址
2 数据链路层 互连设备之间传送和识别数据帧。 数据帧与比特流之间的转换
1 物理层 以"0"、"1"代表电压的高低、灯光的闪灭。界定连接器和网线的规格。 比特流与电子信号之间的切换

②TCP/IP四层(或五层)模型(实际)

TCP/IP 是一组协议的代名词,TCP是传输层的协议,IP是网络层的协议,因为这两个协议是核心所以起名TCP/IP协议。但这个模型它还包括许多协议,TCP/IP 通信协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求

层级名称 对应系统/功能归属 包含协议/设备 详细定义 (来自图片内容)
应用层 应用程序 DNS, HTTP, FTP, SMTP, Telnet 等 负责应用程序间沟通。 如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。网络编程主要就是针对应用层
传输层 OS TCP, UDP 负责两台主机之间的数据传输。 如传输控制协议(TCP),能够确保数据可靠的从源主机发送到目标主机。
网络层 OS IP, ICMP, ARP、路由器 负责地址管理和路由选择。 例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。
数据链路层 设备和驱动程序 令牌环网、以太网, 交换机 负责设备之间的数据帧的传送和识别。 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测、数据差错校验等工作。
物理层 设备和驱动程序 集线器(Hub) 负责光/电信号的传递方式。 比如网线(双绞线)、光纤、wifi无线网络的电磁波等。

注:

  1. 整个协议栈通过上表可以看出涉及硬件、驱动、OS和应用程序,贯穿整个计算机
  2. 当硬件或者OS不同时也可以互相通信,关键就在于都遵守同一个网络协议栈(如:TCP/IP),硬件厂商生产的设备要遵守底层的规则,OS也要满足对应实现(OS中的网络模块)

2. 网络传输的基本流程(TCP/IP)

具体封装和分用在数据的封装与分用(TCP/IP)这一节介绍

①网络传输流程图

  1. 网络传输流程图(同一个网段,无需路由转发)

    两台同一网段主机之间通过网络进行数据传递,是由用户层自顶而下,经过网卡A传递给网卡B时,再由网卡B自底而上交付给用户层。

  2. 网络传输流程图(不同网段,要经过一个或多个路由器)

②以太网

  • 每台主机在局域网中都有一个唯一标识,也就是MAC地址。当一台主机想和另一台主机通信时,会标出源MAC地址和目的MAC地址<src:MAC, dst:MAC>,主机会向局域网中直接发生信息,接收的主机检查是否和目的MAC地址对应,如果对应就接收,否则丢弃
  • 以太网中,任何时刻,只允许一台机器向网络中发送数据。如果由多台同时发送,会发生数据碰撞。一旦检测到冲突,设备会立即停止发送,并通过算法等待一段随机的时间后再次尝试。
  • 没有交换机的情况下,一个以太网就是一个碰撞域。所有发送数据的主机要进行碰撞检测和碰撞避免。

注:

  1. 网卡的混杂模式:无差别接收所有局域网中的数据(数据链路层不再对比mac地址,而是将所有的报文全部都向上传输) 这也就是抓包的本质
  2. 令牌环网已经淘汰,不做介绍,本质就是谁有令牌谁能发信息,然后循环获得令牌
  3. 以太网和令牌环网为什么可以相互交流?
    本质就是因为分层是的各层之间不干扰。同时路由器有多张网卡,通过不同网卡发挥不同作用,并且做格式转换

3. IP地址和MAC地址

以下默认IPv4,除特殊标记

  1. IP地址(网络层)
    • 作用:标识网络中不同的主机。
    • 版本:IPv4 (4字节) 和 IPv6
    • 格式:点分十进制,如 192.168.0.1
    • IP地址是用来标识全球范围内,主机的唯一性(这里的IP地址指的是公网IP)
  2. 可以使用ifconfig查看Linux中的IP地址和MAC地址。其中inet就是IPv4的地址,ether就是MAC地址。eth代表以太网

注意:在网络传输过程中,源IP和目的IP通常保持不变

  1. MAC地址 (数据链路层)

    • 作用:识别数据链路层相连的节点。
    • 格式:48位,6个字节,16进制表示,如 08:00:27:03:fb:19。
    • 特性:网卡出厂时确定,理论上全球唯一(虚拟机可能会冲突)
    • 关键点:MAC地址只在局域网内有效。每经过一个路由器,数据帧的MAC地址就会改变(因为路由器会拆包重发)
  2. 可以使用ipconfig /all来查看Windows上的MAC地址,IP地址

  3. 两者的区别 :MAC和IP

    • IP地址像是你要去哪个城市。如"北京 -> 上海"
    • MAC地址像是在当前路段谁负责送(每一跳都变)。如"北京快递员" -> "中转站"

4. 数据的封装与分用(TCP/IP)

网络中核心的概念封装和分用:"封装"就是打包快递,"分用"就是拆快递。

  • 不同的协议层对数据包有不同的称谓
    • 在传输层:(数据)段(segment)
    • 在网络层:数据报 (datagram)
    • 在链路层:(数据)帧(frame).
  • 封装:应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),这一步就是封装(Encapsulation)。(首部信息中包含了一些类似于首部有多长, 载荷(payload)有多长, 上层协议是什么等信息)
  • 分用:数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 "上层协议字段" 将数据交给对应的上层协议处理,这一步是分用。(所以任何一层协议都要有两个能力将报头和有效载荷分离的能力和向上层那个协议交付有效载荷)
  1. 封装的过程,贯穿协议栈

    • 应用层:拿到用户数据。
    • 传输层:加上TCP/UDP首部(包含端口号) -> 变成了段 (Segment)。
    • 网络层:加上IP首部(包含IP地址) -> 变成了数据报 (Datagram)。
    • 链路层:加上以太网首部(包含MAC地址)和尾部 -> 变成了帧 (Frame)。
    • 物理层:把帧转换成光电信号(0101...)发出去。
  2. 数据分用的过程,贯穿协议栈

    • 物理层:收到光电信号,转成数据帧。
    • 链路层:检查MAC地址是不是自己,是就拆掉以太网头,根据"上层协议字段"把数据交给网络层。
    • 网络层:检查IP地址,拆掉IP头,根据"协议值"把数据交给传输层。
    • 传输层:根据端口号,把数据交给对应的应用程序。
    • 应用层:拿到原始数据。

二、套接字🌐

1. 端口号、TCP/UDP协议和网络字节序概念

①端口号

  1. 源IP地址和目的IP地址都在IP数据报首部(MAC的源地址和目的地址是在帧首部)

  2. 端口号:标识主机上的哪一个进程,告诉OS,当前的这个数据要交给哪一个进程来处理

    • 一个端口号只能被一个进程占用,一个进程可以绑定多个端口
    • 2字节16位的整数,0 ~ 1023是知名端口(HTTP、FTP...),1024 ~ 65535是OS动态分配端口(客户端常用)
  3. 端口号和进程ID(PID)区别:一方面前者用于网络功能后者是OS中的进行解耦。另一方面并不是所有进程都有通信

  4. 传输层协议(TCP和UDP)的数据段中有两个端口号,源端口号和目的端口号分别是数据是谁发的,要发给谁

结论:

  • IP地址 + 端口号 能够标识网络上的某一台主机的某一个进程即唯一标识网络上一个进程。网络通信的本质就是两个进程通信,所以要唯一的表示两个进程
  • 网络通信的本质是进程通信,而进程间通信(IPC)我在前面也介绍过,通信的前提就是看到同一份资源,而这个资源就是网络。之前又说Linux中一切皆文件,所以网络也是一个文件,通信就是和网络这个文件进行IO。
  • Socket = IP + Port,而我们想要访问网络功能就需要使用系统调用(群众中有坏人,所以提供接口)。同时在传输层拿到端口号放入哈希函数中计算,得出访问哈希表中的那个进程,哈希表存的是一个指针(task_struct*)

②TCP/UDP协议------传输层协议

  1. TCP(Transmission Control Protocol传输控制协议):适合对数据完整性、顺序性要求高的场景

    • 面向连接
    • 可靠传输
    • 面向字节流 SOCK_STREAM
  2. UDP (User Datagram Protocol用户数据报协议):适合追求实时性、可容忍丢包的场景

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

③网络字节序

  1. 内存中的多字节数据相对于内存地址有大端和小端之分,而网络数据流同样有大端小端之分,但是如果不要求大端还是小端那么通信就是一个麻烦,TCP/IP协议规定网络数据流为大端字节序(低地址高字节)。如果是小端就要转换成大端再接收或向网络发生数据。

  2. 可以调用以下库函数做网络字节序和主机字节序的转换:包含头文件#include <arpa/inet.h>

函数名 参数类型(返回值同) 方向 功能描述
htons uint16_t 主机 -> 网络 Host to Network Short 将 16 位的主机字节序转换为网络字节序。通常用于设置端口号。
htonl uint32_t 主机 -> 网络 Host to Network Long 将 32 位的主机字节序转换为网络字节序。通常用于设置 IPv4 地址。
ntohs uint16_t 网络 -> 主机 Network to Host Short 将 16 位的网络字节序转换为主机字节序。通常用于读取接收到的端口号。
ntohl uint32_t 网络 -> 主机 Network to Host Long 将 32 位的网络字节序转换为主机字节序。通常用于读取接收到的 IPv4 地址。
  • h:host(主机)
  • n:network(网络)
  • s:short(16位,通常指端口号)
  • l:long(32位,通常指IP地址)
  • to:转换方向

2. 网络编程中所用到的主要结构sockaddr

sockaddr结构

引入:套接字编程的种类有三种:网络套接字编程(用户间的网络通信)、域间套接字编程(同一个机器)、原始套接字编程(网络工具)。而我们在编写程序的时候希望网络接口统一抽象化,参数类型是一样的

  1. IPv4 网络地址结构体(对应网络和原始套接字编程)struct sockaddr_in

    • _in 代表 Internet。这是专门用于 IPv4 网络通信的地址结构。
    • 16位地址类型:AF_INET 明确告诉系统这是 IPv4 协议。AF_INET6是IPv6地址类型
    • 16位端口号:指定通信端口
    • 32位IP地址:指定 IPv4 地址
    • 原始套接字编程针对网络工具编写,直接搞底层。网络套接字编程是用户间网络通信
  2. Unix 域地址结构体(对应域间套接字)struct sockaddr_un

    • _un 代表 Unix。这是专门用于同一台机器内部进程通信的地址结构。
    • 16位地址类型:AF_UNIX 明确告诉系统这是本地通信。
    • 108字节路径名:因为本地通信是靠文件路径标识的(如 /tmp/mysql.sock),所以这里存的是文件路径字符串,而不是 IP 地址。
  3. 通用结构体(对应所有种类struct sockaddr

    作用:通用接口(父类)这是所有套接字地址结构体的通用模板。

    在调用系统函数(如 bind, connect, accept)时,函数的参数通常都定义为 struct sockaddr * 类型。

    实际使用中:我们很少直接操作这个结构体来存数据,而是把具体的结构体强制转换成这个类型传进去。用于系统函数调用的参数类型转换。

结论:socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in。这样的好处是接口的通用性,可以接收IPv4、IPv6、以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数,就是通过前两个字节,接口就可以区分是那种套接字传进来了。

sockaddr:

cpp 复制代码
struct sockaddr 
{
    sa_family_t sa_family;  // 标识符,告诉OS接下来的14个字节是什么。可能的取值AF_INET、AF_INET6
    char        sa_data[14];// 如果是IPv4:2字节端口号 + 4字节IP地址 + 8字节填充
}

sockaddr_in:

虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in。

cpp 复制代码
struct sockaddr_in {
    sa_family_t    sin_family;   // AF_INET    这些类型都是经过typedef
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IP(网络字节序)
    char           sin_zero[8];  // 填充
};

in_addr结构

in_addr :用来表示一个IPv4的IP地址,其实就是一个32位的整数

cpp 复制代码
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

3. UDP接口---一个UDP服务器

接下来会通过写一个简单的UDP服务器和客户端介绍各个接口的应用

①网络通信接口

  1. socket和close:socket创建通信端点,申请一个socket文件描述符。第一步。 (TCP/UDP,客户端和服务器)
cpp 复制代码
#include <sys/types.h>       
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数:
	1. domain:协议族。通常填 AF_INET (IPv4) 或 AF_INET6 (IPv6)。
	2. type:套接字类型。TCP填SOCK_STREAM,UDP填SOCK_DGRAM。
	3. protocol:具体协议。通常填 0

返回值:
	1. 成功:返回一个非负整数(套接字描述符),类似于文件描述符(fd)。
	2. 失败:返回 -1。


close:关闭sockfdf
#include <unistd.h>
int close(int fd);
  1. bind:绑定地址和端口。第二步 (TCP/UDP,服务器)
cpp 复制代码
#include <sys/types.h>         
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
	1. socket:socket() 函数返回的套接字描述符。
	2. addr:指向通用地址结构体sockaddr的指针。
	   通常需要把具体的sockaddr_in强制转换为 sockaddr*传入。这里包含了 IP 地址和端口号
	3. addrlen:地址结构体的长度(字节数),通常填 sizeof(struct sockaddr_in)。
返回值:
	1. 成功:返回 0
	2. 失败:返回 -1
  1. recvfrom从指定的套接字接收数据。(面向数据报)
cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
								struct sockaddr *src_addr, socklen_t *addrlen);
参数:
	1. sockfd:套接字。
	2. buf:缓冲区。指定存放接收到的消息的内存地址。
	3. len:指定buf参数的大小(即最多能接收的字节数)。
	4. flags:用于控制消息的接收方式,默认填0阻塞方式。
	5. src_addr:源地址,输出型参数。发送方的源地址。如果不需要源地址,可设为NULL
	6. addrlen:源地址长度。
返回值:
	1. 成功:返回实际接收到的消息长度(以字节为单位)
	2. 失败:返回-1,设置错误码
注:ssize_t == long int
  1. sendto发送一个信息到指定的套接字中
cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数:同recvfrom,但是dest_addr输入型参数。借用这个输入性参数判定发送给谁

②其他接口

  1. popen在调用程序和要执行的Shell命令之间创建一个单向的管道,使得程序能够像读写普通文件一样,读取外部命令的输出或向外部命令写入数据
c 复制代码
#include <stdio.h>
FILE *popen(const char *command, const char *type);
参数:
	1. command:一个以null结尾的字符串,包含要执行的Shell命令
	2. type:指定管道的 I/O 模式,以下两种之一:r w

返回值:
	1. 成功,返回一个指针
	2. 失败,返回NULL
  1. bzero将字符串s的前n个字节清零
c 复制代码
void bzero(void *s, size_t n);
参数说明:
	1. s:要置零的数据的起始地址(指针)
	2. n:要置零的数据字节个数
  1. 将点分十进制格式的字符串转换为 32 位的二进制网络字节序(大端序)无符号长整型数的三个函数
c 复制代码
// inet_addr-这个函数是有问题的255.255.255.255这里不是合法地址,只适用IPv4
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);  
返回值:
	1. 成功,返回网络字节序的二进制地址
	2. 传入非法的IP字符串、空字符串或NULL,返回INADDR_NONE
	3. 如果传入单个空格" ",返回 0


// inet_aton适用IPv4头文件同上
int inet_aton(const char *cp, struct in_addr *inp);
参数:
	1. cp:输入的IP地址字符串
	2. inp:指向struct in_addr结构体的指针,用于接收转换后的二进制地址
返回值:
	成功返回非零值,失败返回 0


// inet_pton支持IPv4和IPv6的地址转换
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数:
	1. af:指定地址族。AF_INET(IPv4)或AF_INET6(IPv6)
	2. src:输入的IP字符串。
	3. dst:输出的二进制地址缓冲区。
返回值:
	成功返回 1,无效输入返回 0,系统错误返回-1。
  1. 二进制地址转点分十进制IP字符串
c 复制代码
// inet_ntoa适用于IPv4,线程不安全
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
参数:
	1. 输入的struct in_addr结构体,包含要转换的二进制IPv4地址
返回值:
	指向静态缓冲区的字符指针,该缓冲区在每次调用时会被覆盖

// inet_ntop适用于IPv4和IPv6,线程安全
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
	1. af:指定地址族,AF_INET(IPv4)或AF_INET6(IPv6)
	2. src:指向二进制地址的指针,可以是struct in_addr*或struct in6_addr*
	3. dst:指向输出缓冲区的指针,用于接收转换后的IP字符串。
	4. len:输出缓冲区的大小。
返回值:
	成功返回指向输出缓冲区的指针,失败返回 NULL

③服务器和客户端

UDP 网络编程,无需维护连接状态

核心类封装 (UdpSocket)

  1. socket():创建 SOCK_DGRAM 类型的 socket。
  2. bind():服务器绑定端口。
  3. sendto/recvfrom:发送和接收数据

介绍:客户端发指令,服务器执行

五个文件:Log.hpp Main.cxx Makefile UdpClient.cc UdpServer.hpp

UdpServer.hpp: 服务器

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"

// using func_t = std::function<std::string(const std::string&)>;  // 和下面typedef一样
typedef std::function<std::string(const std::string &)> func_t;

Log lg;

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
// 127.0.0.1 本地环回地址:服务器启动后,这个进程只能进行本地通信,贯穿本机网络协议栈但不往网络中发,又上去了
// 127.0.0.1 通常用来进行客户端和服务器的测试
// 客户端也用这个127.0.0.1访问
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : sockfd_(0), port_(port), ip_(ip), isrunning_(false)
    {
    }

    void Init()
    {
        // 1. 创建udp socket  第一步创建
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET == PF_INET
        if (sockfd_ < 0)
        {
            lg(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
        }
        lg(Info, "socket create success, sockfd: %d", sockfd_);

        // 2. bind socket    第二步绑定端口号和IP地址
        struct sockaddr_in local;                       // 存所要绑定的信息,等下传给bind
        bzero(&local, sizeof(local));                   // 把local开始的sizeof(local)字节清成0
        local.sin_family = AF_INET;                     // 表明套接字类型  sin => socket inet
        local.sin_port = htons(port_);                  // 需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的IP+port
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 用户会传点分十进制所以要转1. string -> uint32_t  =>  2. uint32_t必须是网络序列的  inet_addr这个库函数就完成了这两部分
        // local.sin_addr.s_addr = htonl(INADDR_ANY); // 可以不用上面的方式,即传个默认IP地址又转的,可以直接这种方式接口。INADDR_ANY就是全0,也就是可以不用htonl

        
        if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    void Run(func_t func) 
    {
        isrunning_ = true; // 服务器是一直运行的,设置一个变量

        char inbuffer[size]; // 存放接受来的消息
        while (isrunning_)
        {
            struct sockaddr_in client; // 输出型参数,获取发送方的信息
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }

            inbuffer[n] = 0; // 当字符串看,结尾设置0
            std::string info = inbuffer;
            std::string echo_string = func(info);  // 在外部处理接收的字符串
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len); // 发送一个信息到指定的套接字中
        }
    }
    ~UdpServer()
    {
        if (sockfd_ > 0)
            close(sockfd_);
    }

private:
    int sockfd_;     // 网路文件描述符
    // 云服务器运行起来的禁止直接绑定公网IP,因为有些机器有多张网卡,即多个IP,如果只绑定一个IP可能导致其他的IP发的信息接收不到
    // 告诉操作系统:"我不关心具体是哪个 IP,只要发往这台机器当前网卡的任何 IP 的请求,且端口匹配,我都接收。"
    std::string ip_; // 任意地址bind 默认使用0就可以
    uint16_t port_;  // 表明服务器进程的端口号
    bool isrunning_;
};

Main.cxx:

cpp 复制代码
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

bool SafeCheck(const std::string &cmd)
{
    int safe = false;
    std::vector<std::string> key_word = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while"
    };
    for(auto &word : key_word)
    {
        auto pos = cmd.find(word);
        if(pos != std::string::npos) return false;
    }

    return true;
}

std::string ExcuteCommand(const std::string &cmd)
{
    std::cout << "Received messages: " << cmd << std::endl;
    if(!SafeCheck(cmd))
        return "bad man";

    FILE *fp = popen(cmd.c_str(), "r");  // 在这一步获取命令,然后就会创建子进程执行命令
    if(nullptr == fp)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while(true)
    {
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if(ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);

    return result;
}

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

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init();
    svr->Run(ExcuteCommand);

    return 0;
}

UdpClient.cc:客户端

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./udpclient serverip serverport   必须要知道服务器的IP和端口号,要连接
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 填充服务器的相关信息
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport); 
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);


    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

    // client也要绑定,但是不用显示调用bind,一般OS自由随机选择,因为自己绑定可能出现冲突,避免恶意软件绑定所有端口号
    // 首次发送数据的时候bind
    string message;
    char buffer[1024];
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);

        // 1. 数据 2. 给谁发server
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);
        

        // 输出型参数,介绍从那台服务器来的消息,未来可能不是一个服务器发来,如上面多次sendto
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);
    return 0;
}

Log.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <cstdarg>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


// 用法,直接用Log定义一个对象(这里可以定义打印方式),用函数对象的形式传入参数调用即可

// 定义日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

// 输出方式
#define Screen 1
#define OneFile 2
#define SortFile 3


#define LOG_MODE 0666
#define SIZE 2048

// 输出到一个文件的文件名   输出到多个文件时,可以加日志等级作为后缀,进行区分
#define LogFile "log.txt"


// 成员变量有两个,一个是打印方法  一个是文件路径
class Log
{
public:
    Log(int printMethod = Screen, std::string path = "./log/")
        : _printMethod(printMethod), _path(path)
    {}

    // 改变输出方式,使用者设置
    void Enable(int method)
    {
        _printMethod = method;
    }

    // 根据等级转字符串
    std::string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // 调用打印方式:屏幕 一个文件 根据日志等级的不同文件
    void PrintLog(int level, const std::string &logtxt)
    {
        switch (_printMethod)
        {
        case Screen:
            std::cout << logtxt;
            break;
        case OneFile:
            PrintOneFile(LogFile, logtxt);
            break;
        case SortFile:
            PrintSortFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    // 写在一个文件中
    void PrintOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = _path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, LOG_MODE);
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

	//根据日志等级不同,写在多个文件中
    void PrintSortFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += '.';
        filename += LevelToString(level);
        PrintOneFile(filename, logtxt);
    }

    // 可变参数  va_list va_start va_end va_arg
    void operator()(int level, const char *format, ...)
    {
        // 获取时间戳 参数是输出型参数和返回值一样
        time_t t = time(nullptr);

        // 传一个时间戳指针,返回一个结构体类型指针。这个时间是1900年1月1日开始
        // 这个结构体的变量除了月的天从1开始,其他都从0开始
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];

        // 限制长度组成字符串
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", LevelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        // 用可变参数组成字符串 注意这里的s需要va_list va_start va_end(即创建 初始化 置空)
        // int vsnprintf(char *str, size_t size, const char *format, va_list ap);
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 存储时间字符串和可变参数组成的字符串
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // 调用打印参数 一个日志等级,一个字符串参数
        PrintLog(level, logtxt);
    }

    ~Log()
    {}

private:
    int _printMethod;
    std::string _path;
};

Makefile:

cpp 复制代码
.PHONY:all
all:udpserver udpclient

udpserver:Main.cxx
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++11


.PHONY:clean
clean:
	rm -f udpserver udpclient

④做一个UDP的聊天室

修改上述的服务器和客户端。Udp 的socket是全双工的,允许被同时读写的,所以就要把客户端改成多线程,不能阻塞在输入哪里。然后一个终端输出所有客户端发送的信息,另一个终端输出自己发送的信息

Terminal.hpp:不能都在一个终端显示,另开一个终端显示所有客户端发送的信息。然后重定向到标准错误,把所有客户端发送到服务器的信息直接重定向到标准错误

cpp 复制代码
  // 也就是再向标准错误输出信息时,这个信息就会显示在/dev/pts/6这个终端上
  // 也可以在执行程序时,带一个2>/dev/pts/6指令,这样就和下面的代码是一样的 
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

std::string terminal = "/dev/pts/6";  // 在/dev/pts这个目录写有多个终端文件

int OpenTerminal()
{
    int fd = open(terminal.c_str(), O_WRONLY); // 打开其中一个终端文件
    if(fd < 0)
    {
        std::cerr << "open terminal error" << std::endl;
        return 1;
    }
    dup2(fd, 2);   // 把终端文件重定向到标准错误位置
    return 0;
}

UdpClient.cc:把客户端改成多线程,同时收发信息

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;
};

void *recv_message(void *args)
{
    // OpenTerminal();
    ThreadData *td = static_cast<ThreadData *>(args);
    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
}

void *send_message(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    string message;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += " comming...";
    sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);

    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);

        // std::cout << message << std::endl;
        // 1. 数据 2. 给谁发
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}

// 多线程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct ThreadData td;
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport); //?
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

    td.serverip = serverip;

    pthread_t recvr, sender;
    pthread_create(&recvr, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recvr, nullptr);
    pthread_join(sender, nullptr);

    close(td.sockfd);
    return 0;
}

Main.cxx:

cpp 复制代码
#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init();
    svr->Run();

    return 0;
}

UdpServer.hpp:同上面的区别在于增加了一个容器,进行用户的添加和广播给所有用户信息

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "Log.hpp"

typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;

Log lg;

enum{
    SOCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip),isrunning_(false)
    {}
    void Init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); 
        if(sockfd_ < 0)
        {
            lg(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
        }
        lg(Info, "socket create success, sockfd: %d", sockfd_);
        // 2. bind socket
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); 
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); 

        if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }
    void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport)
    {
        auto iter = online_user_.find(clientip);
        if(iter == online_user_.end())
        {
            online_user_.insert({clientip, client});
            std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
        }
    }

    void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
    {
        for(const auto &user : online_user_)
        {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += std::to_string(clientport);
            message += "]# ";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
        }
    }

    void Run() 
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            
            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            CheckUser(client, clientip, clientport);

            std::string info = inbuffer;
            Broadcast(info,clientip, clientport);
        }
    }
    ~UdpServer()
    {
        if(sockfd_>0) close(sockfd_);
    }
private:
    int sockfd_;     // 网路文件描述符
    std::string ip_; // 任意地址bind 0
    uint16_t port_;  // 表明服务器进程的端口号
    bool isrunning_;
    std::unordered_map<std::string, struct sockaddr_in> online_user_;
};

4. TCP接口---服务器和客户端

①网络通信接口

服务器所用接口:初始化动作要1.socket、2.bind和listen

  1. listen:开始监听。调用此函数将套接字从"主动"变为"被动",准备接受连接请求。 (TCP, 服务器)
cpp 复制代码
#include <sys/types.h>    
#include <sys/socket.h>

int listen(int sockfd, int backlog);
参数:
	1. sockfd:已经绑定好的套接字描述符。
	2. backlog:等待连接队列的最大长度。当服务器正忙于处理一个连接时,其他客户端的连接请求会在这个队列里排队。如果队列满了,新的连接请求会被拒绝。
返回值:
	1. 成功:返回 0。
	2. 失败:返回 -1。
  1. accept:接收请求 。这是一个阻塞函数,程序运行到这里会停下来等待,直到有客户端发起连接。返回值重要
cpp 复制代码
#include <sys/types.h>         
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
	1. sockfd:处于监听状态的套接字描述符。
	2. addr:(输出参数)用于获取客户端的地址信息(IP 和端口)。如果不需要客户端信息,可填 NULL。
	3. addrlen:(输入/输出参数)传入时是addr的长度,返回时会被填充为实际地址长度。
返回值:
	1. 成功:返回一个新的非负整数(新的套接字描述符)。
	   注意:这个新的描述符才是真正用于和该客户端通信的,原来的sockfd继续用于监听新的连接。
	2. 失败:返回 -1。

注意:这个返回值才是用于通信的。sockfd的任务只是接待所以我们叫它监听套接字

读写用文件那一套就可以,因为都是流。即read和write

客户端所用接口 :1.socket、2.connect

  1. connect:建立连接 客户端调用此函数主动向服务器发起"三次握手" (TCP, 客户端)
cpp 复制代码
#include <sys/types.h>         
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
	1. sockfd:客户端创建的套接字描述符。
	2. addr:指向服务器地址结构体的指针(包含服务器的 IP 和端口)。
	3. addrlen:地址结构体的长度。
返回值:
	1. 成功:返回 0(表示连接建立成功)。
	2. 失败:返回 -1

②守护进程

  1. 会话Session

    • 会话:用户通过终端(如 Xshell)登录系统时,系统会为其创建一个会话
    • SID:会话的ID(SID,多个登陆即多个会话也需要管理起来)。用户登录后运行的第一个进程(如 bash)会成为该会话的"会话首进程",其PID即为SID
    • 作用:一个会话可以包含多个进程组(任务)。它将用户登录产生的所有任务统一管理。
  2. 进程组(Process Group)

    • 概念:进程组是一组相关进程的集合,通常是为了完成某个具体的"任务"
    • PGID:进程组ID,通常等于该组"组长进程"的PID。当使用一条命令(或管道 |)启动多个进程时,Shell会将它们放入同一个进程组,第一个进程就是组长进程,即使组长进程先退出,只要组内还有其他进程,该进程组就依然存在。如果只启动一个进程,它自成一组,组长就是它自己。
  3. 进程组 vs 任务

    • 任务是交由进程组来完成的。有些任务只需要一个进程(如 ls),有些任务则需要一组进程协作。上面说了,一个会话有多个进程组(一个前台进程组和一个后台进程组),也就是多个任务(一个前台任务和多个后台任务)
  4. 前后台任务切换一个会话中,只能有一个前台任务(进程组),键盘输入的数据只会发送给前台任务,前台和后台任务都可以向显示器(标准输出)打印信息。ctrl+c对后台进程无效

    • 刚登陆时:bash是前台进程
    • 启动后台任务:在命令末尾加 &(如 ./program &),Shell 会返回任务号和 PID。此时程序在后台运行,终端控制权立刻交还给bash。如果不加&,则该任务成为前台任务,bash成为后台任务,此时再输入命令就运行不了
    • 切回前台(fg):使用fg [任务号],可以将后台任务重新拉到前台
    • 暂停任务(Ctrl+Z):在前台运行程序时按下 Ctrl+Z使其暂停。此时终端失去前台任务,bash 会自动"顶出来"接管终端。
    • 恢复后台运行(bg):使用bg [任务号],可以让被 Ctrl+Z 暂停的任务在后台继续运行。
  5. Linux与Windows的会话差异

    • Windows:会话是用户级别的。注销用户时,系统会关闭该用户的所有会话和进程
    • Linux:在后台运行的进程,会受到用户登录和注销的影响

守护进程化就是不想让我们的后台进程受到任何用户登录和注销的影响。自成进程组,自成会话的后台进程,我们称为守护进程

setsid:主要用于创建一个全新的会话,使进程彻底脱离原有的控制终端、进程组和会话的束缚,即守护进程化。注意:不准进程组组长调用,为了解决这个问题就需要用fork,用子进程调用

cpp 复制代码
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
返回值:
	1. 成功:返回新创建的会话ID(SID),该值实际上等于调用进程的进程 ID(PID)。
	2. 失败:返回-1,并设置错误码

daemon:是Linux系统编程中用于创建守护进程(Daemon)的便捷库函数。它本质上是对 fork()、setsid() 等一系列底层操作的封装,帮助程序快速脱离控制终端,并在后台稳定运行

cpp 复制代码
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数:
	1. nochdir:
		为0:函数会将进程的当前工作目录更改为根目录(/)。这是为了防止守护进程占用可卸载的文件系统挂载点
		为非 0:保持当前工作目录不变。
	2. noclose(控制标准 I/O 流):
		为0:函数会将stdin、stdout、stderr重定向到/dev/null。确保守护进程不会接收任何终端输入,也不会向终端输出任何信息
		为非 0:保持标准输入、输出和错误流不变

返回值:
	1. 成功:返回0
	2. 失败:返回-1,并设置错误码

上面的库函数daemon的作用实际同下面这个文件内容Daemon.hpp

Daemon.hpp:

cpp 复制代码
// 守护进程化
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    // 1. 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 将自己变成独立的会话
    if (fork() > 0)
        exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());

    // 4. 标准输入,标准输出,标准错误重定向至/dev/null(垃圾桶)
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

守护进程生成的程序名一般以d结尾

③服务器和客户端

这里写的是一个翻译服务器,即客户端输入英文,服务器处理后返回中文

一共10个文件:TcpServer.hpp TcpClient.cc ThreadPool.hpp Task.hpp Makefile Main.cc Log.hpp(这个文件同上面的日志类就不再贴下来了) Init.hpp Daemon.hpp(这个文件就是上面贴的守护进程化) dict.txt

TcpServer.hpp: 服务器实现了多个版本的,主要步骤就是InitServer(socket、bind、listen),Start(守护进程化、创建多线程、accept、创建任务结构体、push任务)

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;    
extern Log lg;

enum
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError,
};

// 声明
class TcpServer;

// 用于多线程版本
class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t) 
        : sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}

public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr;     // 解决静态成员函数中不能调用其他成员函数问题,直接把对象this传进来
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const std::string &ip = defaultip)
        : listensock_(defaultfd), port_(port), ip_(ip)
    {
    }

    // 创建套接字listensock_(只用一次)、bind、listen
    void InitServer()
    {
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensock_: %d", listensock_);


        // 防止偶发性的服务器无法进行立即重启 ,这里先不做解释
        int opt = 1;
        setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); 


        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &(local.sin_addr));  // <==> local.sin_addr.s_addr = INADDR_ANY;
        if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind socket success, listensock_: %d", listensock_);


        // Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }

        lg(Info, "listen socket success, listensock_: %d", listensock_);
    }

    // 用于多线程版本
    // static void *Routine(void *args)
    // {
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//???
    //     delete td;
    //     return nullptr;
    // }


    // 获取新连接accept,开始读写
    void Start()
    {
        // signal(SIGPIPE, SIG_IGN);  // 下面的守护进程化做了Daemon
        Daemon();  // 守护进程化
        ThreadPool<Task>::GetInstance()->Start();  // 获取单例,创建多线程

        lg(Info, "tcpServer is running....");
        for (;;)
        {
            // 1. 获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); 
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            // 2. 根据新连接来进行通信
            lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
            // version 1 -- 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);

            // version 2 -- 多进程版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // child
            //     close(listensock_);
            //     if(fork() > 0) exit(0);   // 也可以用信号,忽略
            //     Service(sockfd, clientip, clientport); //孙子进程, system 领养,所以下面的waitpid就可以阻塞等待
            //     close(sockfd);
            //     exit(0);
            // }
            // close(sockfd);
            // // father
            // pid_t rid = waitpid(id, nullptr, 0);


            // version 3 -- 多线程版本
            // ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);

            // version 4 --- 线程池版本
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

    // 用于单进程版、多进程版、多线程版 ------ 这个死循环服务,一般不会这样,一般来一个处理一个
    // void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    // {
    //     // 测试代码
    //     char buffer[4096];
    //     while (true)
    //     {
    //         ssize_t n = read(sockfd, buffer, sizeof(buffer));
    //         if (n > 0)
    //         {
    //             buffer[n] = 0;
    //             std::cout << "client say# " << buffer << std::endl;
    //             std::string echo_string = "tcpserver echo# ";
    //             echo_string += buffer;

    //             write(sockfd, echo_string.c_str(), echo_string.size());
    //         }
    //         else if (n == 0)  // 客户端那边退了,这边也退
    //         {
    //             lg(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
    //             break;
    //         }
    //         else
    //         {
    //             lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
    //             break;
    //         }
    //     }
    // }
    ~TcpServer() {}

private:
    int listensock_;   // 这个套接字用于监听,只有一个即可,主要是accept返回的套接字用于通信
    uint16_t port_;
    std::string ip_;
};

ThreadPool.hpp:线程安全的线程池。主要步骤是获取单例,创建多线程,多线程执行(pop任务执行),push任务

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:

    // 获取单例,创建多线程后 线程开始执行,并且阻塞等待队列中的内容
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();  // 处理任务
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) 
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; 
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

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

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Task.hpp:

cpp 复制代码
// 构建任务,然后跑任务------读写
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"

extern Log lg;
Init init;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
        : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    Task()
    {
    }
    void run()
    {
        char buffer[4096];
        ssize_t n = read(sockfd_, buffer, sizeof(buffer)); 
        if (n > 0)
        {
            buffer[n] = 0;  // 如果用telnet连接要去掉后面的换行符------这个换行符可能占一个或者两个位置
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.translation(buffer);

            // 在读取完成之后写之前,可能客户端突然关闭了,此时向一个关闭的客户端写会出错,
            // 也就是OS可能会直接杀死该进程,在管道那一节博客中说到该问题,此时就需要对该信号忽略在服务器启动时
            n = write(sockfd_, echo_string.c_str(), echo_string.size()); 
            if(n < 0)
            {
                lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
            }
        }
        else if (n == 0)
        {
            lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
        }
        else
        {
            lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
        }
        close(sockfd_);
    }
    void operator()()
    {
        run();
    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

TcpClient.cc:客户端这里不仅有连接和读写等操作,还支持重连

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void Usage(const std::string &proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        int cnt = 5; // 重连次数
        int isreconnect = false;
        int sockfd = 0;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            return 1;
        }

        do
        {
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error..., reconnect: " << cnt << std::endl;
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        // 多次重连没连上,只能程序重新启动
        if (cnt == 0)
        {
            std::cerr << "user offline..." << std::endl;
            break;
        }

        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            std::cerr << "write error..." << std::endl;
            continue;
        }

        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        close(sockfd);
    }

    return 0;
}

Main.cc

cpp 复制代码
#include "TcpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    lg.Enable(SortFile);
    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();
    tcp_svr->Start();

    return 0;
}

Makefile:

c 复制代码
.PHONY:all
all:tcpserverd tcpclient

tcpserverd:Main.cc    # 守护进程一般以d结尾
	g++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f tcpserverd tcpclient

Init.hpp:读取dict文件内容存在容器中,调用translation返回翻译结果

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";


static bool Split(std::string &s, std::string *part1, std::string *part2)
{
    auto pos = s.find(sep);
    if(pos == std::string::npos) return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos+1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }
        in.close();
    }
    std::string translation(const std::string &key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end()) return "Unknow";
        else return iter->second;
    }
private:
    std::unordered_map<std::string, std::string> dict;
};

dict.txt:这个文件可有可无,可以自己添加,但是为了程序的完整性贴了一些(注意格式,中间的冒号)

c 复制代码
apple:苹果...
banana:香蕉...
red:红色...
yellow:黄色...
the: 这
be: 是
to: 朝向/给/对
and: 和
I: 我
in: 在...里
that: 那个
have: 有
will: 将
but: 但是
as: 像...一样
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他们
we: 我们
their: 他们的
his: 它的
with: 和...一起
she: 她
he: 他(宾格)
it: 它

5. 其他概念

  1. UDP、TCP协议提供全双工的通信服务:双方可同时读写。双方都有两个缓冲区即发送和接收缓冲区
  2. 三次握手和四次挥手:套接字代码层面是不知道这个过程的。connect和accept返回成功,表明握手成功,close进行两次挥手,两个close四次挥手
  3. 客户端向服务器连接很明显不止一个客户端,所以这些连接也要被先组织再描述

小结

本篇博客的主要内容就是TCP和UDP的接口和为了接收接口写的服务器

TCP服务器:

  1. 服务器初始化
    • 调用socket,创建文件描述符
    • 调用bind,将当前的文件描述符和ip/port绑定在一起。
    • 调用listen,监听,为后面的accept做好准备
  2. 服务器开始接收信息
    • 调用accecpt,并阻塞,等待客户端连接过来
    • 读写操作
      TCP客户端:
    • 调用socket,创建文件描述符
    • 调用connect,向服务器发起连接请求
    • 读写操作

UDP服务器不做介绍