【Linux】网络(上)
目录
- 1. 网络基础1
- 1.1 TCP/IP五层(或四层)模型
- 1.2 路由器、IP地址和Mac地址
- 1.3 网络字节序
- 1.3.1 主机字节序 与 网络字节序 的转化
- 1.4 TCP协议和UDP协议
- 1.5 端口号
- 2. 网络编程套接字
- 2.1 socket 常见API
- 2.1.1 socket() —— 创建套接字
- 2.1.2 bind() —— 绑定 IP 和端口
- 2.1.3 recvfrom() 和 sendto() —— 数据传输(UDP)
- 2.1.3 listen()——监听连接(TCP服务器)
- 2.1.4 accept()、connect() —— 接受、发起连接(TCP)
- 2.1.5 write()、read() —— 发送、接受数据(TCP)
- 2.1.6 通信流程
- 2.2 sockaddr结构
- 2.3 地址转换函数
- 2.4 使用UDP实现一个简单的聊天室
- 2.4.1 UDP服务器端
- 2.4.2 UDP客户端
- 2.5 用TCP实现一个单词翻译器(含守护进程化)
- 2.5.1 前台进程和后台进程
- 2.5.2 用TCP实现一个单词翻译器的代码实现
1. 网络基础1
1.1 TCP/IP五层(或四层)模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇.
TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求.
- 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤, 现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等. 集线器(Hub)工作在物理层.
- 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、帧同步(就是说从网线上检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就自动重发)、数据差错校验等工作. 有以太网、令牌环网, 无线LAN等标准. 交换机(Switch)工作在数据链路层.
- 网络层: 负责地址管理和路由选择. 例如在IP协议中, 通过IP地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.
- 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.
- 应用层: 负责应用程序间沟通,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等. 我们的网络编程主要就是针对应用层
OS,网络协议栈网络协议栈和我们之前学习的OS有什么关系?: 如图关系
网络通信的本质:就是贯穿协议栈的过程。
通信的过程,本质就是不断的封装和解包的过程!
大部分协议的共性,未来我们学习具体协议的时候,都会涉及这两个问题。只有这样我们面对封装和解包,才不会困惑。
- 几乎任何层的协议,都要提供一种能力,将报头和有效载荷分离的能力
- 几乎任何层的协议,都要在报头中提供,决定将自己的有效载荷交付给上层的哪一个协议的能力
以太网传输信息的原理:
1.2 路由器、IP地址和Mac地址
跨网段的主机的文件传输. 数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器.
- IP协议屏蔽了底层网络的差异化,其实靠的就是工作在IP层的路由器!底层都连通这路由器,路由器应对了底层的差异。
- ip地址,尤其是目的IP,一般都是不会改变的,协助我们进行路径选择。
然而Mac地址,出局域网之后,源和目都要被丢弃,让路由器重新封装。 - IP地址:从哪来,到哪去———一直是不变的
Mac地址:上一站从哪里来,下一站去哪里,会一直变化,变化的依据是“我要去哪里”
网络通信的基本脉络示意图:
1.3 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
1.3.1 主机字节序 与 网络字节序 的转化
- 这些函数名很好记,
h
表示host,n
表示network,l
表示32位长整数,s
表示16位短整数。 - 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
1.4 TCP协议和UDP协议
- 此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
- 此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
1.5 端口号
端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
1.网络协议中的下三层,主要解决的是,数据安全可靠的送到远端机器
2.是用户使用应用层软件,完成数据发送和接受的。→先把这个软件启动起来(进程)!
所以我们日常网络通信的本质:就是进程间通信!!
端口号 vs 进程pid
pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号???
1.不是所有的进程都要网络通信,但是所有进程都要有pid
2.系统和网络功能解耦
- 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
因为端口号是用哈希表获得的。 - 服务端的端口号必须是客户端都知道的
2. 网络编程套接字
2.1 socket 常见API
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6
2.1.1 socket() —— 创建套接字
适用于TCP/UDP, 客户端 + 服务器
int socket(int domain, int type, int protocol);
参数说明
-
domain
(协议族/地址族)
指定通信的协议域,常见值:AF_INET
:IPv4 网络协议(如192.168.1.1
)。AF_INET6
:IPv6 网络协议。AF_UNIX
或AF_LOCAL
:本地进程间通信(Unix 域套接字)。
-
type
(套接字类型)
指定数据传输方式:SOCK_STREAM
:面向连接的流套接字(TCP,可靠、按序传输)。SOCK_DGRAM
:无连接的数据报套接字(UDP,不可靠、有大小限制)。SOCK_RAW
:原始套接字(直接操作网络层,如自定义协议)。
-
protocol
(具体协议)
通常设为0
,表示根据domain
和type
自动选择默认协议。例如:domain=AF_INET
+type=SOCK_STREAM
→ 默认协议是IPPROTO_TCP
。domain=AF_INET
+type=SOCK_DGRAM
→ 默认协议是IPPROTO_UDP
。
返回值
非负整数,成功:返回新创建的套接字文件描述符(Socket File Descriptor)
-1,失败:返回错误代码,可通过 errno 获取具体原因
2.1.2 bind() —— 绑定 IP 和端口
作用:
将套接字与指定的本地IP地址和端口号绑定,适用于TCP/UDP, 服务器
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
参数说明
参数名 | 类型 | 说明 |
---|---|---|
socket | int | 要绑定的套接字文件描述符(由socket() 函数创建) |
address | struct sockaddr* | 指向包含绑定地址信息的结构体指针 |
address_len | socklen_t | 地址结构体的实际长度(以字节为单位) |
地址结构体(IPv4示例):
struct sockaddr_in {sa_family_t sin_family; // 地址族(AF_INET)in_port_t sin_port; // 端口号(网络字节序)struct in_addr sin_addr; // IP地址(网络字节序)char sin_zero[8]; // 填充字段(通常置0)
};
返回值
0:绑定成功
-1 :绑定失败,错误代码存入 errno
示例代码:
int server_fd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡,INADDR_ANY=0.0.0.0
address.sin_port = htons(8080); // 绑定8080端口if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) {perror("bind failed");exit(EXIT_FAILURE);
}
注意事项
-
客户端程序通常不需要调用
bind()
,系统会自动分配端口 -
服务器必须绑定明确端口
-
端口号小于1024需要
root
权限 -
使用
INADDR_ANY
表示绑定本机所有网络接口 -
端口号和IP地址需要使用网络字节序(
htons()/inet_addr()
转换)
2.1.3 recvfrom() 和 sendto() —— 数据传输(UDP)
这两个函数是用于无连接(UDP)套接字通信的核心函数。
recvfrom() —— 接收数据(UDP)
功能
从已连接或未连接的套接字接收数据,并获取发送方的地址信息。
函数原型
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
参数
-
sockfd: 套接字描述符
-
buf: 接收数据的缓冲区
-
len: 缓冲区长度
-
flags: 控制标志(通常为0)
-
src_addr: 用于保存发送方地址信息的结构体指针
-
addrlen: 指向地址结构体长度的指针(输入时为缓冲区大小,输出时为实际地址长度)
返回值
-
成功: 返回接收到的字节数
-
连接关闭: 返回0
-
出错: 返回-1并设置errno
sendto() —— 发送数据(UDP)
功能
向指定目标地址发送数据,主要用于无连接(UDP)套接字。
函数原型
#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);
参数
-
sockfd: 套接字描述符
-
buf: 要发送的数据缓冲区
-
len: 要发送的数据长度
-
flags: 控制标志(通常为0)
-
dest_addr: 目标地址结构体指针
-
addrlen: 目标地址结构体长度
返回值
- 成功: 返回发送的字节数
- 出错: 返回-1并设置errno
注意事项
- 这两个函数主要用于UDP通信,但也可用于已连接的UDP套接字
- 对于已连接的UDP套接字,可以使用recv()/send()替代
- 数据报可能会丢失、重复或乱序到达
- 接收缓冲区大小应足够容纳整个数据报,否则多余部分会被丢弃
- 在多线程环境中使用时需要注意线程安全问题
巧记:
- 两个函数的地址结构体(sockaddr结构体)部分用的都是别人的结构体,不是自己的结构体。
- 发送函数需要在发送前就明确发送对象,所以发送函数的地址结构体是输入型参数,是被发送对象的准确地址结构体。
- 接受函数接受前不需要知道发送方的结构体,所以接受函数的结构体是输出型参数,收到信息后,就会知道发送方的信息
2.1.3 listen()——监听连接(TCP服务器)
函数原型:
int listen(int sockfd, int backlog);
参数说明
-
sockfd
- 要监听的 Socket 文件描述符,通常由 socket() 创建,并已通过 bind() 绑定到某个 IP 和端口。
-
backlog
-
等待连接队列的最大长度,即内核为此 Socket 排队的最大未完成连接数。
-
如果队列已满,新的连接请求会被拒绝(客户端收到 ECONNREFUSED 错误)。
-
典型值:5、10 或 SOMAXCONN(系统定义的最大值,通常为 128 或更高)。
-
返回值
成功:返回 0。
失败:返回 -1,并设置 errno(如 EBADF、EADDRINUSE 等)。
2.1.4 accept()、connect() —— 接受、发起连接(TCP)
accept()—— 接受连接(TCP)
作用
从已完成连接队列中取出一个连接,返回一个新的套接字描述符,用于与该客户端通信。
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd:监听中的套接字(由 socket() 创建并通过 listen() 进入监听状态)。
addr:指向 struct sockaddr 的指针,用于存储客户端的地址信息(可设为 NULL 表示不获取)。
addrlen:输入时为 addr 缓冲区大小,输出时为实际地址长度(若 addr 为 NULL,可设NULL)。
返回值:
成功:返回一个新的套接字描述符(专用于TCP的数据传输)。
失败:返回 -1,并设置 errno(如 EAGAIN 表示无连接可接受)。
connect() —— 发起连接(TCP)
作用
客户端向指定服务器发起连接请求。
函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd:客户端套接字(由 socket() 创建)。
addr:指向服务器地址结构体(如 struct sockaddr_in)。
addrlen:服务器地址结构体的长度。
返回值
成功:返回 0。
失败:返回 -1,并设置 errno(如 ECONNREFUSED 表示服务器拒绝连接)
2.1.5 write()、read() —— 发送、接受数据(TCP)
write()——发送数据(TCP)
ssize_t write(int fd, const void *buf, size_t count); // 发送数据
参数说明:
fd :文件描述符(如 Socket 的 sockfd:accept函数返回值)
buf:存储数据的缓冲区
count:缓冲区大小(最多写入的字节数)
返回值:
大于0: 实际读取的字节数
= 0:连接已关闭(TCP 对端调用了 close())
-1:出错,检查 errno(如 EAGAIN 表示无数据可读)
read()——接收数据(TCP)
ssize_t read(int fd, void *buf, size_t count); // 接收数据
参数说明:
fd :文件描述符(如 Socket 的 sockfd:accept函数返回值)
buf:缓冲区
count:请求读取的字节数
返回值:
大于0:实际读取的字节数(可能小于 count)。
= 0:连接关闭或文件末尾
-1:读取失败,并设置 errno 表示错误原因(如 EINTR 被信号中断、EAGAIN 非阻塞模式无数据等)。
2.1.6 通信流程
UDP:
服务器端/客户端
1. socket() → 2. bind()(可选) → 3. recvfrom()/sendto() → 4. close()
TCP:
服务器端:
1. socket() → 2. bind() → 3. listen() → 4. accept() → 5. recv()/send() → 6. close()
客户端:
1. socket() → 2. connect() → 3. send()/recv() → 4. close()
2.2 sockaddr结构
struct sockaddr
是 Socket
编程中通用的地址结构体
IPv4
和IPv6
的地址格式定义在netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位地址类型, 16位端口号和32位IP地址.IPv4
、IPv6
地址类型分别定义为常数AF_INET
、AF_INET6
. 这样,只要取得某种sockaddr
结构体的首地址,不需要知道具体是哪种类型的sockaddr
结构体,就可以根据地址类型字段确定结构体中的内容.socket API
可以都用struct sockaddr *
类型表示, 在使用的时候需要强制转化成sockaddr_in
; 这样的好处是程序的通用性, 可以接收IPv4
,IPv6
, 以及UNIX Domain Socket
各种类型的sockaddr
结构体指针做为参数;
以下是 sockaddr、sockaddr_in(IPv4)、sockaddr_in6(IPv6)和 sockaddr_un(Unix域套接字)的结构体定义及说明:
- 通用套接字地址结构 sockaddr
#include <sys/socket.h>struct sockaddr {sa_family_t sa_family; // 地址族(如 AF_INET、AF_INET6、AF_UNIX)char sa_data[14]; // 协议特定地址信息(如 IP + 端口或路径)
};
用途:用于类型强制转换,所有具体地址结构(如 sockaddr_in)必须能转换为 sockaddr 以兼容套接字函数(如 bind()、connect())。
- IPv4 地址结构 sockaddr_in
#include <netinet/in.h>struct sockaddr_in {sa_family_t sin_family; // 地址族(必须为 AF_INET)in_port_t sin_port; // 16位端口号(网络字节序)struct in_addr sin_addr; // 32位 IPv4 地址(网络字节序)char sin_zero[8]; // 填充字段(通常置 0)
};struct in_addr {uint32_t s_addr; // IPv4 地址(32位网络字节序)
};
用途:存储 IPv4 地址和端口号。
示例:
struct sockaddr_in addr;
memset(addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 端口 8080
addr.sin_addr.s_addr = inet_addr("192.168.1.1"); // IP 地址
- IPv6 地址结构 sockaddr_in6
#include <netinet/in.h>struct sockaddr_in6 {sa_family_t sin6_family; // 地址族(必须为 AF_INET6)in_port_t sin6_port; // 16位端口号(网络字节序)uint32_t sin6_flowinfo; // 流信息(通常为 0)struct in6_addr sin6_addr; // 128位 IPv6 地址uint32_t sin6_scope_id; // 作用域 ID(用于链路本地地址)
};struct in6_addr {unsigned char s6_addr[16]; // IPv6 地址(128位)
};
- Unix 域套接字地址结构 sockaddr_un
#include <sys/un.h>struct sockaddr_un {sa_family_t sun_family; // 地址族(必须为 AF_UNIX 或 AF_LOCAL)char sun_path[108]; // 文件系统路径名(以空字符结尾)
};
用途:用于本地进程间通信(IPC),通过文件系统路径名标识。
示例:
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysocket", sizeof(addr.sun_path) - 1);
结构体 | 地址族(sa_family ) | 用途 | 地址格式 |
---|---|---|---|
sockaddr | 任意(通用) | 类型强制转换 | 通用 |
sockaddr_in | AF_INET | IPv4 通信 | IPv4 + 端口 |
sockaddr_in6 | AF_INET6 | IPv6 通信 | IPv6 + 端口 |
sockaddr_un | AF_UNIX /AF_LOCAL | 本地进程间通信 | 文件系统路径 |
常见使用场景:
sockaddr_in:用于 TCP/UDP 网络编程(IPv4)。
sockaddr_in6:用于 IPv6 网络编程。
sockaddr_un:用于本地进程间高效通信(如数据库、X11 服务器)。
2.3 地址转换函数
sockaddr_in
中的成员struct in_addr
(sin_addr
),表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr
表示之间转换
字符串转in_addr的函数:
- inet_aton() 函数
int inet_aton(const char *cp, struct in_addr *inp);
参数:
cp:点分十进制格式的 IPv4 地址字符串(如 “192.168.1.1”)
inp:指向要填充的 in_addr 结构体的指针
返回值:
成功返回 1
失败返回 0
- inet_addr() 函数
in_addr_t inet_addr(const char *cp);
- inet_pton() 函数
int inet_pton(int af, const char *src, void *dst);
参数:
af:地址族(IPv4 用 AF_INET,IPv6 用 AF_INET6)
src:IP 地址字符串
dst:指向存储结果的缓冲区(对于 IPv4 是 struct in_addr)
返回值:
成功返回 1
无效输入返回 0
错误返回 -1
in_addr转字符串的函数
- inet_ntoa() 函数
#include <arpa/inet.h>char *inet_ntoa(struct in_addr in);
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢? man手册上说,inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。 那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢?
会产生第二次调用时的结果的会覆盖掉上一次的结果,因为inet_ntoa把结果放到自己内部的一个静态存储区,无论调用多少个inet_ntoa,只会生成一个静态存储区。所以inet_ntoa是非线程安全的
特点:
返回静态缓冲区指针(非线程安全)
不需要预先分配内存
每次调用会覆盖上次结果
- inet_ntop() 函数
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
af:地址族(IPv4用AF_INET,IPv6用AF_INET6)
src:指向in_addr结构体的指针
dst:目标字符串缓冲区
size:缓冲区大小(IPv4至少16字节)
返回值:
成功返回指向dst的指针
失败返回NULL
2.4 使用UDP实现一个简单的聊天室
全部代码的gittee链接:udp
2.4.1 UDP服务器端
含UdpServer.hpp和UdpServer.cc两个文件。
UdpServer.hpp:
#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include "log.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
using namespace std;// using func_t = std::function<std::string(const std::string&)>;
typedef function<string(const string &)> func_t;
Log lg;enum
{SOCKET_ERR = 1,BIND_ERR,};string DefaultIp = "0.0.0.0";
uint16_t DefaultPort = 8080;
const int SIZE_ = 1024;
class Udpserver
{
public:Udpserver(uint16_t port = DefaultPort, string ip = DefaultIp) : ip_(ip), port_(port){}void Init(){// 创建upd socket(套接字)sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // UDP,打开了网卡文件if (sockfd_ < 0){lg(Fatal, "socket create error,socket:%d", sockfd_);exit(SOCKET_ERR);}lg(Info, "socket create success,socket:%d,error:%s", sockfd_, strerror(errno));// bind socketstruct sockaddr_in local;bzero(&local, sizeof(local)); // 相当于memsetlocal.sin_family = AF_INET; // 地址族local.sin_port = htons(port_); // 端口号local.sin_addr.s_addr = INADDR_ANY; // IP,表示绑定所有网卡,INADDR_ANY=0.0.0.0if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind socket error,socket:%d,error: %s", sockfd_, strerror(errno));exit(BIND_ERR);}lg(Info, "bind socket success,socket:%d", sockfd_);}// 检查用户是否在用户表中,如果没在,添加到用户表中void CheckUser(const struct sockaddr_in &cliaddr, const string &clientip, uint16_t clientport){auto iter = online_users_.find(clientip);if (iter == online_users_.end()){online_users_.insert({clientip, cliaddr});cout << "[" << clientip << ":" << clientport << "]" << "add to online users" << endl;}}void Broadcast(char *buffer_in, const string &clientip, uint16_t clientport){string echo_message = "[";echo_message += clientip;echo_message += ":";echo_message += to_string(clientport);echo_message += "]";echo_message += "send to a message:";echo_message += buffer_in;for (auto user : online_users_){sendto(sockfd_, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr *)&user.second, sizeof(user.second));}}void Run(){isrunning_ = true;char buffer_in[SIZE_];cout << "Running begin!" << endl;while (isrunning_){struct sockaddr_in cliaddr;// 接收信息unsigned int len = sizeof(cliaddr);int n = recvfrom(sockfd_, buffer_in, sizeof(buffer_in) - 1, 0, (struct sockaddr *)&cliaddr, &len);if (n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}buffer_in[n] = 0;// 整理发消息的用户信息string clientip = inet_ntoa((struct in_addr)cliaddr.sin_addr); // inet_ntoa:将网络地址从二进制格式(struct in_addr)转换为点分十进制字符串格式的函数uint16_t clientport = ntohs(cliaddr.sin_port);CheckUser(cliaddr, clientip, clientport);// 将信息发给每个用户Broadcast(buffer_in,clientip,clientport);}}~Udpserver(){if (sockfd_ >= 0)close(sockfd_);}private:int sockfd_; // 网络文件描述符string ip_;uint16_t port_; // 表明服务端的端口号bool isrunning_;unordered_map<string, struct sockaddr_in> online_users_;
};
UdpServer.cc:
#include <memory>
#include"UdpServer.hpp"
#include <cstdio>
#include<cstring>
using namespace std;void Usage(string proc)
{cout<<"\n\tUsage : "<<proc<<" ServerIp ServerPort"<<endl;
}
//./udpserver serverport
int main(int argc,char* argv[])
{if(argc<2){Usage(argv[0]);exit(1);}uint16_t port=stoi(argv[1]); //stoi:字符串转化为整数std::unique_ptr<Udpserver> svr(new Udpserver(port)); //在类内,port会被转化为网络字节序svr->Init();svr->Run();return 0;
}
2.4.2 UDP客户端
含UdpClient.cc一个文件。
UdpClient.cc:
#include<iostream>
#include<string>
#include<cstring>
#include <unistd.h>
#include<functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include"Terminal.hpp"
using namespace std;//./UdpClient serverip serverport
void Usage(string proc)
{cout<<"\n\tUsage : "<<proc<<" ServerIp ServerPort"<<endl;
}struct ThreadData
{struct sockaddr_in server;string serverip;int sockfd;
};void* recv_message(void* args)
{//OpenTerminal(); //使发消息和收消息的窗口分开char buffer[1024];ThreadData* td=static_cast<ThreadData*>(args);struct sockaddr_in server=td->server;string serverip=td->serverip;int sockfd=td->sockfd;while(true){unsigned int len=sizeof(server);int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&server, &len);if (n > 0){buffer[n] = '\0';cerr << buffer << endl; //用的是cerr,因为send_message部分有cout输出}}return nullptr;
}void* send_message(void* args)
{string message;ThreadData* td=static_cast<ThreadData*>(args);struct sockaddr_in server=td->server;string serverip=td->serverip;int sockfd=td->sockfd;while(true){cout<<"Please Enter@";getline(cin,message);int n=sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));}return nullptr;
}int main(int argc,char* argv[])
{if(argc<3){Usage(argv[0]);exit(1);}//完善server的地址结构体string serverip=argv[1];uint16_t serverport=stoi(argv[2]); //stoi:将字符串转化为整数struct sockaddr_in server;bzero(&server, sizeof(server)); //相当于memset server.sin_addr.s_addr=inet_addr(serverip.c_str()); //将 IPv4 点分十进制字符串(如 "192.168.1.1")转换为 32 位网络字节序的整数值。server.sin_family=AF_INET;server.sin_port=htons(serverport);//创建套接字int sockfd=socket(AF_INET,SOCK_DGRAM,0); //UDP,打开了网卡文件if(sockfd<0){cout<<"socket create error"<<endl;exit(1);}struct ThreadData td;td.server=server;td.serverip=serverip;td.sockfd=sockfd;pthread_t thread1,thread2;pthread_create(&thread1, nullptr, recv_message,(void*)&td);pthread_create(&thread2, nullptr, send_message, (void*)&td);pthread_join(thread1, nullptr);pthread_join(thread2, nullptr);close(sockfd);return 0;
}
2.5 用TCP实现一个单词翻译器(含守护进程化)
2.5.1 前台进程和后台进程
- 前台后台都可以向显示器打印,但谁可以从标准输入中获取数据(即谁拥有键盘),谁就是前台文件。
- 一个会话只有一个前台进程,也只有一个bash进程。将某一后台进程提为前台,则原前台进程变为后台。
- 前后进程的转化
2.5.2 用TCP实现一个单词翻译器的代码实现
全部代码链接:用TCP实现一个单词翻译器
代码有超过500行,不在这全展示了,展示关键代码:
Tcpserver.hpp:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <functional>
#include "Log.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include"ThreadPool.hpp"
#include"Task.hpp"
#include"Daemon.hpp"
using namespace std;string defaultIp="0.0.0.0";
uint16_t Defaultport=8080;
const int backlog = 10; // 但是一般不要设置的太大Log lg;enum
{SOCKET_ERR = 1,BIND_ERR,LISTEN_ERR,UsageError
};class TcpServer; //先声明一下,防止ThreadData中不认识 TcpServerclass ThreadData
{
public:ThreadData(int fd,uint16_t port,string ip,TcpServer* tc):sockfd(fd),clientport(port),clientip(ip),tcsr(tc){}int sockfd;uint16_t clientport;string clientip;TcpServer* tcsr; //放入自己的this指针,方便调用成员函数(Service)
};class TcpServer
{
public:TcpServer(uint16_t port=Defaultport,string ip=defaultIp):ip_(ip),port_(port){}void ServerInit(){// 创建upd socket(套接字)listensock_ = socket(AF_INET, SOCK_STREAM, 0); // TCP,打开了网卡文件 if (listensock_ < 0){lg(Fatal, "socket create error,socket:%d", listensock_);exit(SOCKET_ERR);}lg(Info, "socket create success,socket:%d,error:%s", listensock_, strerror(errno));// bind socket,自己的struct sockaddr_in local;bzero(&local, sizeof(local)); // 相当于memsetlocal.sin_family = AF_INET; // 地址族local.sin_port = htons(port_); // 端口号inet_aton(ip_.c_str(), &(local.sin_addr)); //这段代码是用于将点分十进制的IP地址字符串转换为网络字节序(大端序)的32位二进制形式, //并存储到sockaddr_in结构体的sin_addr字段中// local.sin_addr.s_addr = INADDR_ANY; // IP,表示绑定所有网卡,INADDR_ANY=0.0.0.0 //if (bind(listensock_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind socket error,socket:%d,error: %s", listensock_, strerror(errno));exit(BIND_ERR);}lg(Info, "bind socket success,socket:%d", listensock_);//前面都和Udp没啥区别,tcp多了一个监听listen//Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态if(listen(listensock_,backlog)<0){lg(Fatal,"listen socket error,socket:%d,error:%s",listensock_,strerror(errno));exit(LISTEN_ERR);}lg(Info, "listen socket success,socket:%d",listensock_);}void Start(){Daemon();ThreadPool<Task>::GetInstance()->Start(); //线程池必须放前面,不能放在循环里lg(Info, "tcpServer is running....");for(;;){// 1. 获取新连接,accept----------拉客人struct sockaddr_in client;bzero(&client, sizeof(client)); // 相当于memsetsocklen_t len=sizeof(client);int sockfd=accept(listensock_,(struct sockaddr*)&client,&len);//这个sockfd用于通信,这一步是阻塞模式if(sockfd<0) //listensock_错误或accept调用失败,{lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));sleep(1); continue;}uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip)); // in_addr转字符串的函数// 2. 根据新连接来进行通信-----------服务客人lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);// version 1 -- 单进程版// Service(sockfd,clientport,clientip);// close(sockfd);//多进程版,给孙子进程执行读写程序,因为孙子进程不用等待,进程的文件表都是独立的,所以在退出前,全关了// pid_t id=fork();// if(id==0) //child// {// close(listensock_);// if(fork()>0) // {// close(sockfd); // exit(0); //父进程// }// //孙子进程// Service(sockfd,clientport,clientip);// close(sockfd);// exit(0);// }// close(sockfd);// // 非阻塞地回收子进程// while (waitpid(-1, nullptr, WNOHANG) > 0);// version 3 -- 多线程版本// pthread_t pid;// ThreadData* td=new ThreadData(sockfd,clientport,clientip,this);// pthread_create(&pid,nullptr,Routine,(void*)td);// version 4 --- 线程池版本Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->PushTask(t);}}~TcpServer() {}private:int listensock_; //不可用于TCP的数据传输uint16_t port_;string ip_;
};
守护进程代码:
Daemon.hpp:
#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>//守护进程(Daemon)
// 守护进程的特点:
// 脱离终端:不和任何终端关联(不会受用户注销影响)。
// 后台运行:没有交互界面,默默执行任务。
// 独立会话:不受父进程或终端信号干扰。
// 通常以 root 权限运行:可以管理系统资源(如 Web 服务器、数据库服务)。//void (*signal(int signum, void (*handler)(int)))(int);
// signum:要处理的信号编号。
// handler:信号处理函数。
//SIG_IGN 表示忽略该信号。const std::string nullfile = "/dev/null";
// /dev/null 是一个特殊的设备文件,它的行为:
// 读取时:立即返回 EOF(文件结束)。
// 写入时:直接丢弃数据(像黑洞一样)。//Daemon()一运行,那么就开启了守护进程void Daemon(const std::string &cwd = "") //cwd不传入参数,则表示不更改当前调用进程的工作目录
{// 1. 忽略其他异常信号signal(SIGCLD, SIG_IGN); //SIGCLD(或 SIGCHLD)是 子进程状态变更信号(例如子进程退出时触发),代码作用:防止子进程退出时变成僵尸进程(Zombie)signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN); //这些都是适合忽略的信号,防止这些信号导致进程退出//终止守护进程,可以通过kill命令// 2. 将自己变成独立的会话if (fork() > 0)exit(0);setsid();// setsid() 创建新会话// setsid() 会让当前进程:// 成为新会话的领头进程(Session Leader)。// 脱离原终端的控制(不再受 Ctrl+C、终端关闭等影响)。// 不再有控制终端(TTY),完全独立运行。// 3. 更改当前调用进程的工作目录if (!cwd.empty())chdir(cwd.c_str()); //int chdir(const char *path) : 将当前进程的工作目录更改为 path。// 4. 标准输入,标准输出,标准错误重定向至/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}// 这段代码的作用是 将进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)重定向到 /dev/null,// 确保守护进程不会从终端读取输入或向终端输出任何内容。// 守护进程的需求:// 作为后台服务,它不应依赖终端输入(如键盘)。// 它的输出(如日志)应写入文件或系统日志(如 syslog),而非终端。// 避免对应终端设备的关闭导致进程阻塞或崩溃。
}