文章目录
- 一、前置
- 二、Udpsocket
-
- 2.1创建套接字
- 2.2绑定套接字
- 2.3udp读取数据
- 2.4udp发送数据
- 2.5netstat命令
- 2.6单进程服务端代码解析
- 2.7单进程客户端
- 2.8多线程客户端
-
- [2.8.1UDP socket的特性](#2.8.1UDP socket的特性)
- 2.8.2单线程弊端
- 2.8.3代码分析
- 三、Tcpsocket
- 四、守护进程
一、前置
1.1理解源IP地址和目的IP地址
IP地址
IP地址是互联网上每一个设备的唯一标识符
- 在同一个网络中,每个设备的IP地址必须是唯一的
- 公网IP在全球范围内唯一
- 路由器根据IP地址决定数据包转发路径
- 包含网络部分和主机部分
- 便于路由和地址管理
源IP地址
- 数据包的发送方IP地址
- 标识数据从哪里来
目的IP地址
- 数据包的接收方地址
- 标识数据要到哪里去
以一个例子来阐述源IP地址和目的IP地址
- 数据从福建宁德(源IP)发往云南大理(目的IP),这个旅程依靠网络协议栈的分层协作完成。IP协议负责全局寻址,它封装的数据包标明了始发地与最终目的地,这两个IP地址在整个传输过程中保持不变,如同包裹上始终写着"从宁德寄往大理"。
- 然而,这个数据包并非直达,它需要经过多个中转站(路由器)。在每一段本地旅程中,以太网协议 负责接力运输。当数据包抵达一个路由器(如福州站)后,路由器会拆开以太网协议的"本地包装",查看IP地址来确定下一站的方向。接着,它会为数据包套上一个全新的"本地包装"------即重写源MAC地址 和目的MAC地址。源MAC地址变为当前路由器(福州)的地址,目的MAC地址则变为下一个路由器(如南昌)的地址。
- 这个过程在每一个中转站重复:IP地址指引着最终方向,而MAC地址则在每一段区间内完成实际的交付。就这样,经过一次次MAC地址的接力重写,数据包最终抵达了目的IP地址所在的网络,完成了从宁德到大理的整个通信任务。
1.2认识端口号
网络通信的本质不是机器间进行通信
- 网络协议的下三层,主要解决的是数据安全可靠的送到远端主机
- 用户使用应用层软件,完成数据发送和接受
- 应用层软件本质就是启动起来的进程
- 日常网络通信的本质就是进程间通信--公共资源就是网络--使用网络资源提供的接口就是网络协议栈
- 进程通过网络来向另一个主机的进程发送链接和请求之类的

- 从客户端将请求发给服务端要经过网络协议栈传输
- 传输层如何直到要交给应用层的哪个应用呢,所以传输层和应用层就需要协商一种方案让我们的数据准确的交给上层
- 这种方案就叫做端口号
- 所以客户端想向服务端发送消息的时候就需要在报头中添加服务端对应的端口号,同层协议忽略底层的差异所以这个端口号就相当于直接交给了传输层,对方解析客户端的报头并且把有效载荷提取出来,根据端口号就能交给上层的哪一个应用了,对方给我发消息的时候也需要在报头我的端口号
- 端口号无论对于客户端还是服务端,都能唯一的标识该主机上的唯一一个进程
端口号是传输层协议的内容
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理
- IP地址+端口号能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
在公网上IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一一个进程
ip+port=标识全网的唯一一个进程
这种基于ip+端口号的通信方式我们叫做socket
1.3理解端口号和进程ID
进程pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号
- 不是所有的进程都要网络通信,所有的进程都要有pid
- pid每次都会变化,如果固定就要修改很多东西
- pid是进程管理模块的,端口号是网络模块的,如果使用pid来进行传输的话,上层对pid进行修改那么网络方面势必也要修改这就很麻烦了
- 所以端口号的实现达到了系统和网络方面的解耦
传输层是如何通过端口号来寻找到上层的进程的呢
- 可以理解为传输层内部维护了一张哈希表,节点都存放着进程pcb的指针
- 进程绑定端口号的本质拿端口号在哈希表进行哈希运算,如果位置被占用,就不能用这个端口号,如果没被占用就把进程pcb的地址填入到节点当中
客户端,如何直到服务器的端口号是多少?反过来也一样
每一个服务的端口号必须是众所周知,精心设计,被客户端所知晓的,在安装的时候其实就把对应的端口号给内置进去了
- 一个进程可以绑定多个端口号
- 但是一个端口号不能被多个进程绑定
端口号是用来标识进程的唯一性的,我一个人可以用很多的标识符,比如身份证学号等,但是身份证号不能被多个人使用
1.4认识TCP协议
TCP是传输控制协议,先对其认识
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
TCP面向连接最典型的场景就是TCP在通信的时候需要和客户端发起建立连接的请求,建立连接成功之后才能发数据
比如你和你同学打电话的时候两个人都说声喂,确定双方说的话对方都能听到,这个过程就是建立连接的过程,保证我们通信的信道是通畅的手段,这叫面向连接
TCP可靠传输其中一种就是当数据丢失的时候传输层会自动补发,应用层就不需要管了,前提是网络要保证联通
1.5认识UDP协议
UDP是用户数据报协议,先对其认识
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
UDP面向数据报浅显的理解就是数据报是有边界的,发一个对方就得收一个,比如发邮件一发就是一个完整的邮件
UDP无连接就是直接就发,不需要关心对方的准备状态
UDP不可靠传输就是发给对方对方有没有收到自身的传输层是不清楚的,丢包还是乱序UDP完全不管
- 可靠和不可靠其实是中性词,就像物理上的惰性气体也没有好坏之分
- 可靠是要有很大的成本的,不可靠意味着很简单
- TCP在这个数据包丢失之前都不敢把这个数据包直接丢弃要在传输层一直维护起来,重传也有分几次和周期
- UDP我发了就是发了,后面我也不管,设计和维护就会变得非常简单
UDP在信息派发的场景下应用的比较多,TCP在保证数据安全的场景下应用的比较多
1.6网络字节序(端口转化)
我们知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分,那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保持
- 因此,网络数据流的地址应这样规定;先发送的数据是低地址,后发生的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机,都会按照TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可
比如int是4个字节,低地址放高字节就是大端,发数据给对方的时候对方是不知道是大端还是小端的,就会导致数据不一致问题
有部分少量的字段我们是要自己发给对方的所以就要做大小端的转化,可以调用下列库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //返回值:以网络字节序表示的32位整数
uint16_t htons(uint16_t hostshort); //返回值:以网络字节序表示的16位整数
uint32_t ntohl(uint32_t netlong); //返回值:以主机字节序表示的32位整数
uint16_t ntohs(uint16_t netshort); //返回值:以主机字节序表示的16位整数
1.7socket编程接口
//创建socekt文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain,int type,int protocol);
//绑定端口号(TCO/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//开始监听socket(TCP,服务器)
int listen(int sockfd, int backlog);
//接受请求(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
1.8sockaddr结构
套接字编程的种类
- 域间套接字编程--同一个机器内(本地通信)
- 原始套接字编程--网络工具
- 网络套接字编程--用户间的网络通信

struct sockaddr *addr:就是将网络接口统一抽象化(参数的类型必须是统一的)
这三种套接字的第一个字段都是16位地址类型,都是起始地址
内部会对类型做判断,所以在上层用通用的接口,内部判断来进行区分,这也是典型的多态的特点
二、Udpsocket
2.1创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
创建一个套接字
domain:创建套接字的域,套接字是有类型的,针对不同的场景就需要不同的套接字比如AF_INET使用ipv4的网络协议
以下是常见的场景
- AF_INET: 用于 IPv4 网络协议之间的通信,这是目前最常用的互联网地址族。
- AF_INET6: 用于 IPv6 网络协议之间的通信,旨在解决 IPv4 地址枯竭的问题。
- AF_UNIX (或 AF_LOCAL): 用于同一台主机上的进程间高效通信。
- AF_PACKET: 用于在数据链路层直接收发原始数据包,可用于抓包或自定义协议。
- AF_NETLINK: 用于用户空间进程与内核空间模块之间进行通信的接口。
type:套接字对应的类型
- SOCK_STREAM: 提供面向连接的、可靠的、双向字节流的通信,这是 TCP 协议所使用的类型。
- SOCK_DGRAM: 提供无连接的、不可靠的固定长度数据报传输,这是 UDP 协议所使用的类型。
- SOCK_RAW: 提供对底层网络协议(如 IP 或 ICMP)的直接访问,允许用户自行构造协议头部。
- SOCK_SEQPACKET: 提供面向连接的、可靠的、保留消息边界的固定最大长度数据报传输。
- SOCK_RDM: 提供一种可靠的数据报层,但不保证数据包的传输顺序。
protocol:最终指定要使用的具体网络协议,当domain和type组合在一起已经可以唯一确定一种协议时,此参数可以指定为0,表示使用默认协议
- PPROTO_TCP : 用于指定使用 TCP 传输协议,通常与
AF_INET和SOCK_STREAM配对使用。 - IPPROTO_UDP : 用于指定使用 UDP 传输协议,通常与
AF_INET和SOCK_DGRAM配对使用。 - IPPROTO_SCTP : 用于指定使用 SCTP 传输协议,这是一种提供多宿和多流特性的可靠传输协议。
- IPPROTO_ICMP : 用于指定使用 ICMP 协议,通常与
AF_INET和SOCK_RAW配对以直接处理网络控制消息。 - IPPROTO_ICMPV6 : 用于指定使用 ICMPv6 协议,用于在 IPv6 环境下的网络控制消息。
- IPPROTO_RAW : 用于指定由用户程序自行构造完整的 IP 数据包头部 ,通常与
AF_INET和SOCK_RAW配对,需要较高权限。
返回值:成功返回套接字描述符,失败返回-1,错误码被设置
创建一个套接字的本质在底层就是打开一个文件,相当于指向的是网卡对应的文件
2.2绑定套接字
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd:套接字描述符
const struct sockaddr *addr:通用接口
socklen_t addrlen:结构体对应的长度

#include <strings.h>
void bzero(void *s, size_t n);
将一块指定大小的空间的内容全部设置为0
2.2.1sockaddr_in结构体
sockaddr_in结构体是用于IPV4网络通信的地址信息载体,其核心成员如下
sin_family:指定地址族,通常设置为AF_INET,表示这是用于 IPv4 的地址结构。sin_port:指定16位的端口号,必须使用网络字节序sin_addr:是一个in_addr结构体,其内部的s_addr字段用于存储32位的IPv4地址,必须使用网络字节序sin_zero:一个8字节的填充字段 ,为了使sockaddr_in与更通用的sockaddr结构体大小保持一致,必须将其置零。




##是将左右两个进行拼接,所以sin_family就是这么来的
对方在收到我的消息的时候也需要知道我的端口号才能把消息发送回来,所以结构体当中需要包含端口号,端口号是需要在网络中来回发送的,必须保证端口号是网络字节序列(大端)
2.2.2ip转化
用户比较喜欢使用的ip地址是点分式十进制字符串"0.0.0.0",每个区域的范围是[0,255]所以我们要将字符串转化成32位整型
如何快速的及那个整型ip<->字符串ip

这些计算都是在本机上进行的计算,都是按本机的字节序进行排列
ip是需要客户端和服务端双方进行通晓的,所以也需要转化成网络字节序
现在这种就是低地址高字节,如果需要转化成小端就把part4变成192
头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
字符串转In_addr的函数
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_network(const char *cp);
int inet_pton(int af, const char *src, void *dst);
in_addr转字符串的函数
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
2.3udp读取数据
因为read和write是面对字节流的,但是udp是面对数据报的所以udp收取数据用以下函数
#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);
从一个套接字里收取消息
- buf:缓冲区,收取到的消息放到缓冲区当中
- len:缓冲区大小
- flags:默认设置为0,使用阻塞方式
- struct sockaddr *src_addr:输出型参数,对方的套接字信息放到这个结构体当中,需要我们自己定义来保存
- socklen_t *addrlen:输入输出型参数,结构体长度
- 返回值:成功返回收到了多少个字节,失败返回-1错误码被设置
2.4udp发送数据
#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);
- buf:缓冲区,发送的消息放到缓冲区当中
- len:缓冲区大小
- flags:默认设置为0,使用阻塞方式
- struct sockaddr *dest_addr:输入型参数
- socklen_t *addrlen:输入型参数,结构体长度
- 返回值:成功返回收到了多少个字节,失败返回-1错误码被设置
2.5netstat命令
netstat命令用于监控和分析网络连接
netstat -nlup
用于查看系统中正在监听的 UDP 连接和进程信息
- n:以数字形式显示地址和端口(不进行域名解析)
- l:仅显示监听状态的套接字
- u:仅显示 UDP 协议
- p:显示进程ID和程序名称
- a:显示所有连接(包括监听和已建立的)

- proto:使用的协议
- recv/send:收发报文的个数
- local Address:本地的地址,IP地址+端口号
- foreign address:远端的地址,0.0.0.0:*表示能收到任何客户端发的消息
- state:状态
只要能查到说明服务器已经启动的
2.6单进程服务端代码解析
log.hpp
#pragma once
#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <stdio.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define Info 0
#define Warning 1
#define Error 2
#define Fatal 3
#define Debug 4
#define NUM 1024
#define SCREEN 1
#define ONEFILE 2
#define SORTFILE 3
#define LOGFILE "log.txt"
#define LOGMODE 0666
// 将日志等级转为为字符串
class Log
{
public:
Log()
{
printmethod=SCREEN;
path="./log/";
}
void Enable(int method)
{
printmethod=method;
}
std::string levelTostring(int level)
{
switch (level)
{
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
case Debug:
return "Debug";
default:
return "NONE";
}
}
void operator()(int level, const char *format, ...)
{
// 格式:默认部分+自定义部分
// 日志的等级,格式
struct tm * ltime;
time_t t = time(nullptr);
ltime = localtime(&t);
char leftbuffer[NUM];
snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%d-%d-%d %d:%d:%d]",levelTostring(level).c_str(),
ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,
ltime->tm_hour,ltime->tm_min,ltime->tm_sec);
//用户输入的部分
va_list s;
va_start(s,format);
char rightbuffer[NUM];
vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);
va_end(s);
char logtxt[NUM*2];
snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);
printlog(level,logtxt);
}
void printlog(int level,const std::string &logtxt)
{
switch(printmethod)
{
case SCREEN:
std::cout<<logtxt<<std::endl;
break;
case ONEFILE:
printOnefile(LOGFILE,logtxt);
break;
case SORTFILE:
printSoftfile(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_CREAT|O_WRONLY|O_APPEND,LOGMODE);
if(fd<0) return;
write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printSoftfile(int level,const std::string &logtxt)
{
//文件名
std::string filename=LOGFILE;
filename+=".";
filename+=levelTostring(level);//log.txt.Debug
printOnefile(filename,logtxt);
}
~Log()
{
}
private:
int printmethod;
std::string path;
};
Udpserver.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <string>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <cstdlib>
using namespace std;
#define SIZE 1024
extern Log lg;
enum{
SOCK_ERR=1,
BIND_ERR
};
static const string defaultip="0.0.0.0";
static const uint16_t defaultport=8080;
class Udpserver
{
public:
Udpserver(uint16_t port=defaultport,string ip=defaultip)
:_port(port)
,_ip(ip)
,_isrunning(false)
{
}
void Init()
{
//1.创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd==-1)
{
lg(Fatal,"socket create fail : %s",strerror(errno));
exit(SOCK_ERR);
}
lg(Info,"socket create success");
//2.绑定套接字
struct sockaddr_in sd;
bzero(&sd,sizeof(sd));
sd.sin_family=AF_INET;
sd.sin_port=htons(_port);
sd.sin_addr.s_addr=inet_addr(_ip.c_str());
socklen_t addlen=sizeof(sd);
if(bind(_sockfd,(const struct sockaddr*)&sd,addlen)==-1)
{
lg(Fatal,"bind fail : %s",strerror(errno));
exit(BIND_ERR);
}
lg(Info,"bind success");
}
void Run()
{
_isrunning=true;
char buf[SIZE];
while(_isrunning)
{
//读取数据
struct sockaddr_in src_sd;
socklen_t src_addlen=sizeof(src_sd);
ssize_t n=recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&src_sd,&src_addlen);
if(n==-1)
{
lg(Warning,"recvfrom data fail : %s",strerror(errno));
}
buf[n]=0;
//发送数据
string ads=buf;
string send="server echo $";
send+=ads;
ssize_t s=sendto(_sockfd,send.c_str(),send.size(),0,(const struct sockaddr*)&src_sd,src_addlen);
if(s==-1)
{
lg(Warning,"send data fail : %s",strerror(errno));
}
}
}
~Udpserver()
{
}
private:
int _sockfd;
uint16_t _port;
string _ip;
bool _isrunning;
};

- udp创建套接字采用的是数据报形式来接收的,协议是IPV4协议32位整型的地址
- 我们使用的是网络通信的sockaddr结构,用于网络通信的ip和端口号的服务器都需要告知客户端所以都要采用网络字节序
- sin_addr是结构体内部还有s._addr的成员变量
- 为了泛型化需要统一采用struct sockaddr*来进行转化

- 服务器一般是一直在运行的
- recvfrom和sendto需要使用对方的套接字信息,所以我们要定义变量给它保存起来
- 我们这里将客户端发来的消息回显的发送回去,简单的做数据处理

通过netstat命令我们可以看到8080端口号已经被绑定成功
2.6.1关于ip
unique_ptr<Udpserver> svr(new Udpserver(8080,"113.45.207.93"));
当我们采用使用云服务的ip进行bind的时候

就会出现bind失败的问题
云服务器禁止直接bind公网ip
-
云服务的公网ip是虚拟化的不是真实的
-
一台主机可能会配置多张网卡就意味着有多个ip,如果你bind一个ip就意味着你只能收到发给这个ip的报文,发给其他ip的报文你看不到
-
所以一般都使用bind(ip:0),凡是发给我这台主机的数据,不管是哪个ip,都要根据端口号向上交付--任意地址bind
sd.sin_addr.s_addr=INADDR_ANY;

所以我们设置套接字的时候就可以采用INADDR_ANY,也是表示可以收到任意ip的报文,忽略ip地址直接看端口号
2.6.2关于port
unique_ptr<Udpserver> svr(new Udpserver(80));
unique_ptr<Udpserver> svr(new Udpserver(1023));

当我们bind一些端口号的时候我们会发现我们是bind失败的出错原因是不允许bind没有权限
unique_ptr<Udpserver> svr(new Udpserver(2025));

但是稍大一点的端口号我们都能bind成功
- 0-1023:系统内定的端口号,一般都要有固定的应用层协议使用,http:80,https 443,mysql:3306
- 所以我们要bind的时候一般都要采用1024以上的端口号
2.6.3代码优化(包装器)
我们不单单可以给服务端传送数据,我们还可以向服务端发送命令,服务端在对命令进行处理之后将结果再发回给我们
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <string>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <cstdlib>
#include <unistd.h>
using namespace std;
#define SIZE 1024
extern Log lg;
typedef function<string(const string&)> func_t;
enum{
SOCK_ERR=1,
BIND_ERR,
};
static const string defaultip="0.0.0.0";
static const uint16_t defaultport=8080;
class Udpserver
{
public:
Udpserver(uint16_t port=defaultport,string ip=defaultip)
:_port(port)
,_ip(ip)
,_isrunning(false)
{
}
void Init()
{
//1.创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd==-1)
{
lg(Fatal,"socket create fail : %s",strerror(errno));
exit(SOCK_ERR);
}
lg(Info,"socket create success");
//2.绑定套接字
struct sockaddr_in sd;
bzero(&sd,sizeof(sd));
sd.sin_family=AF_INET;
sd.sin_port=htons(_port);
sd.sin_addr.s_addr=inet_addr(_ip.c_str());
// sd.sin_addr.s_addr=htons(INADDR_ANY);
socklen_t addlen=sizeof(sd);
if(bind(_sockfd,(const struct sockaddr*)&sd,addlen)==-1)
{
lg(Fatal,"bind fail : %s",strerror(errno));
exit(BIND_ERR);
}
lg(Info,"bind success");
}
void Run(func_t fun)
{
_isrunning=true;
char buf[SIZE];
while(_isrunning)
{
//读取数据
struct sockaddr_in src_sd;
socklen_t src_addlen=sizeof(src_sd);
ssize_t n=recvfrom(_sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&src_sd,&src_addlen);
if(n==-1)
{
lg(Warning,"recvfrom data fail : %s",strerror(errno));
continue;
}
buf[n]=0;
//发送数据
string ads=buf;
string send=fun(ads);
cout<<send<<endl;
ssize_t s=sendto(_sockfd,send.c_str(),send.size(),0,(const struct sockaddr*)&src_sd,src_addlen);
if(s==-1)
{
lg(Warning,"send data fail : %s",strerror(errno));
continue;
}
}
}
~Udpserver()
{
if(_sockfd>0)
close(_sockfd);
}
private:
int _sockfd;
uint16_t _port;
string _ip;
bool _isrunning;
};

我们定义了一个包装器把一个函数包装成了一个类型,包装器内部对函数进行了封装所以可以使用()进行直接调用

我们将内部处理数据和对象的进行解耦
main.cpp
#include <memory>
#include "log.hpp"
#include "Udpserver.hpp"
#include <unistd.h>
#include <cstdio>
#include <vector>
using namespace std;
Log lg;
#define BUF_SIZE 4096
void Usage(string proc)
{
cout << "Usage:\n\r" << proc << "port[1024+]" << endl;
}
string Handler(const string & message)
{
string ret="Get a message ";
ret+=message;
return ret;
}
bool Safety(const string&cmd)
{
vector<string> vt={
"rm",
"top",
"kill",
"mv"
};
for(auto& sa:vt)
{
auto pos=cmd.find(sa);
if(pos!=string::npos) return false;
}
return true;
}
string ExcuteCommand(const string & command)
{
cout<<"get a command "<<command<<endl;
if(!Safety(command))
return "false command";
FILE* fp=popen(command.c_str(),"r");
if(fp==nullptr)
{
perror("open error");
return "open err";
}
char buffer[BUF_SIZE];
string ret;
while(1)
{
char* op=fgets(buffer,sizeof(buffer),fp);
if(op==nullptr) break;
ret+=buffer;
}
return ret;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = stoi(argv[1]);
unique_ptr<Udpserver> svr(new Udpserver(port,"127.0.0.1"));
svr->Init();
svr->Run(ExcuteCommand);
}

#include <stdio.h>
FILE *popen(const char *command, const char *type);
- popen:这个函数会为我们创建子进程对我们发送的命令在内部做处理,数据放到FILE*所指向的结构体的缓冲区当中
- command: 命令
- type:打开方式,只读还是只写等等
- fgets的特性是读取一行数据,我们读完一行就放在op当中直到读取到空
- 最终的数据放到string当中

但是有些命令是不被允许的,比如你要杀掉什么进程等等
我们要对字符串做判断如果找到字符串末尾还没有找到就返回成功
2.6.3.1本地环回
127.0.0.1是本地环回地址,数据只在本地计算机内部流转不经过物理网卡,就相当于向下走了一遭又绕回来了,不会影响其他计算机
2.6.4面向多客户端
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <string>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <cstdlib>
#include <unistd.h>
#include <unordered_map>
using namespace std;
#define SIZE 1024
extern Log lg;
typedef function<string(const string &, const string &, uint16_t &)> func_t;
enum
{
SOCK_ERR = 1,
BIND_ERR,
};
static const string defaultip = "0.0.0.0";
static const uint16_t defaultport = 8080;
class Udpserver
{
public:
Udpserver(uint16_t port = defaultport, string ip = defaultip)
: _port(port), _ip(ip), _isrunning(false)
{
}
void Init()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
lg(Fatal, "socket create fail : %s", strerror(errno));
exit(SOCK_ERR);
}
lg(Info, "socket create success");
// 2.绑定套接字
struct sockaddr_in sd;
bzero(&sd, sizeof(sd));
sd.sin_family = AF_INET;
sd.sin_port = htons(_port);
sd.sin_addr.s_addr = inet_addr(_ip.c_str());
// sd.sin_addr.s_addr=htons(INADDR_ANY);
socklen_t addlen = sizeof(sd);
if (bind(_sockfd, (const struct sockaddr *)&sd, addlen) == -1)
{
lg(Fatal, "bind fail : %s", strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success");
}
void CheckUser(const struct sockaddr_in client, const string &clientip, uint16_t &clinetport)
{
auto pos = mp.find(clientip);
if (pos == mp.end())
{
mp.insert({clientip, client});
cout << "[" << clientip << ":" << clinetport << "]" << " #" << "add a new user" << endl;
}
}
void Broadcast(const string &info, const string &clientip, uint16_t &clinetport)
{
string usermessage;
usermessage += "[";
usermessage += clientip;
usermessage += ":";
usermessage += to_string(clinetport);
usermessage += "] ";
usermessage += info;
for (auto &mpclient : mp)
{
socklen_t mpclient_len=sizeof(mpclient.second);
sendto(_sockfd, usermessage.c_str(), usermessage.size(), 0, (const struct sockaddr *)(&mpclient.second), mpclient_len);
}
}
void Run() // func_t fun
{
_isrunning = true;
char buf[SIZE];
while (_isrunning)
{
// 读取数据
struct sockaddr_in client;
socklen_t client_addlen = sizeof(client);
ssize_t n = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&client, &client_addlen);
if (n == -1)
{
lg(Warning, "recvfrom data fail : %s", strerror(errno));
continue;
}
buf[n] = 0;
// 发送数据
// 获取对方的套接字信息
string ads = buf;
string clientip = inet_ntoa(client.sin_addr); // 网络序列转主机序列
uint16_t clinetport = ntohs(client.sin_port);
CheckUser(client, clientip, clinetport);
Broadcast(ads, clientip, clinetport);
}
}
~Udpserver()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd;
uint16_t _port;
string _ip;
bool _isrunning;
unordered_map<string, struct sockaddr_in> mp;
};

-
当我们收取到客户端发送的消息的时候就已经获取到对方的套接字信息了
-
客户端的套接字信息要想在本地使用的话需要将网络序列转主机序列
-
接下来两个函数分别是如果你是第一次访问就把你加入用户列表当中的函数,第二个是将你发的消息给所有访问的客户端发送一份的函数
unordered_map<string, struct sockaddr_in> mp;

- 我们将ip作为key值,套接字作为value值,因为ip是唯一的不会重复
- 通过对ip的查找我们就能确定这个客户端有没有在我们的哈希表当中
- 使用迭代的方式将每个客户端发送的消息向全体客户端发送一份
2.7单进程客户端
client.cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <functional>
using namespace std;
#define SIZE 1024
void Usage(string proc)
{
cout << "Usage:\n\r" << proc <<"ip"<<" port[1024+] " << endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
string ip=argv[1];
uint16_t port=stoi(argv[2]);
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=inet_addr(ip.c_str());
server.sin_port=htons(port);
socklen_t addrlen=sizeof(server);
char buf[SIZE];
string ret;
while(1)
{
cout<<"Please Enter # ";
getline(cin,ret);
ssize_t n=sendto(sockfd,ret.c_str(),ret.size(),0,(const sockaddr*)&server,addrlen);
if(n==-1)
{
cout<<"send fail"<<endl;
continue;
}
struct sockaddr_in temp;
socklen_t taddrlen=sizeof(temp);
ssize_t r=recvfrom(sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&temp,&taddrlen);
if(r>0)
{
buf[r]=0;
cout<<buf<<endl;
}
}
return 0;
}

- 客户端通过命令行来对ip和端口号进行获取
- 客户端需要对服务端的套接字信息进行保存

- 我知道我的客户端要发给哪一个服务端
- 客户端可以用来接收多个服务端发来的信息
客户端bind
- 客户端是不用特定的bind套接字的
- 当客户端调用 sendto() 或 connect() 时,如果socket还没有绑定地址,操作系统会自动完成绑定
- 如果客户端有特定的端口号,这时有人运行一批进程将这批端口号给占用了,那我们的客户端就启动不起来了,所以客户端的端口号由系统随机分配,这样就会减少客户端的端口号冲突,只需要标识客户端的唯一性
- 服务端的端口号是要固定的,因为如果服务端的端口号是随机的,我们就客户端就无法得知服务端的端口号,就无法发送数据
2.8多线程客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <pthread.h>
using namespace std;
#define SIZE 1024
void Usage(string proc)
{
cout << "Usage:\n\r" << proc << "ip" << " port[1024+] " << endl;
}
struct ThreadData
{
int sockfd;
struct sockaddr_in server;
};
void *ClientRecv(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
char buf[SIZE];
while (1)
{
memset(buf, 0, sizeof(buf));
struct sockaddr_in temp;
socklen_t taddrlen = sizeof(temp);
ssize_t r = recvfrom(td->sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&temp, &taddrlen);
if (r > 0)
{
buf[r] = 0;
cerr << buf << endl;
}
}
}
void *ClientSend(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
socklen_t addrlen = sizeof(td->server);
string ret;
while (1)
{
cout<<"Please Enter #";
getline(cin, ret);
ssize_t n = sendto(td->sockfd, ret.c_str(), ret.size(), 0, (const sockaddr *)&td->server, addrlen);
if (n == -1)
{
cout << "send fail" << endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
ThreadData *td = new ThreadData();
string ip = argv[1];
uint16_t port = stoi(argv[2]);
td->sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&td->server, sizeof(td->server));
td->server.sin_family = AF_INET;
td->server.sin_addr.s_addr = inet_addr(ip.c_str());
td->server.sin_port = htons(port);
pthread_t ce, se;
pthread_create(&se, nullptr, ClientRecv, td);
pthread_create(&ce, nullptr, ClientSend, td);
pthread_join(se, nullptr);
pthread_join(ce, nullptr);
return 0;
}
2.8.1UDP socket的特性
1.全双工通信
UDP socket是全双工的,同一个socket描述符可以用于
- 发送数据
- 接收数据
2.无连接特性
- 不需要独立的读写socket
- 同一个socket可以同时与多个对等方通信
2.8.2单线程弊端

1.阻塞导致的相应延迟
getline如果你不输入的话就会阻塞在那里,就无法收取到服务器发来的消息
2.无法处理并发事件
如果线程一阻塞在getline那里线程二给你发送消息就无法即使接收
2.8.3代码分析

- 由于udp套接字的全双工特性允许同时被读写,我们使用两个线程一个处理收到的数据,一个处理发送的数据
- 线程所处理的代码逻辑和单进程所写的代码逻辑相似,这里不做过多的阐述

- 为了防止输入和输出流混在一起打印,我们将收到的数据向标准错误打印


这里我们将服务端经过处理以后的消息我们向另一个终端打印就能实现客户端和服务端分离
原本的消息是向一个终端上打的,经过我们重定向以后就往两个终端上打了
还要保留一个显示器文件还要保存我们发送的数据回显
三、Tcpsocket
Tcp是面向连接的,服务器一般是比较被动的,服务器一直处于一种等待连接到来的状态
3.1监听套接字
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
backlog:底层全连接队列的长度
返回值:成功返回0,失败返回-1,错误码被设置
3.2获取连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:监听的文件描述符
- struct sockaddr *addr:对方的套接字结构体
- socklen_t *addrlen:对方的结构体长度
- 返回值:成功返回整数的文件描述符,失败返回-1,错误码被设置
套接字区别
accept的返回值也是一个文件描述符,创建套接字也有一个文件描述符
我们用一个例子来阐述区别
饭店分为在外面揽客的服务员和在店里招呼客人的服务员
创建套接字的文件描述符就相当于揽客的服务员,因为TCP要等待客户端来连接,所以创建套接字的文件描述符只负责把客户端收进来--把底层的连接获取上来
accept的socket负责在店里为客户端进行服务--提供IO服务/通信服务
3.3telnet命令
telnet [ip] [port]
telnet 是一个用于远程登录和测试网络连接的工具
交互命令:
按 Ctrl+] 进入telnet命令模式
telnet> help # 查看帮助
telnet> quit # 退出
3.4connect连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
connect() 函数用于**建立与远程服务器的连接
int sockfd; // 客户端socket文件描述符
const struct sockaddr *addr; // 服务器地址信息
socklen_t addrlen; // 地址结构体长度
3.5单进程版服务端
tcpserver.cpp
#pragma once
#include <iostream>
#include "log.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
using namespace std;
static const int defaultsockfd = -1;
static const uint16_t defaultport = 8081;
static const string defaultip = "0.0.0.0";
extern Log lg;
enum
{
SOCK_ERR = 1,
BIND_ERR,
Listen_ERR,
};
class TcpServer
{
public:
TcpServer(const uint16_t &port = defaultport, const string &ip = defaultip)
: _listensock(defaultsockfd), _port(port), _ip(ip)
{
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
lg(Fatal, "socket fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(SOCK_ERR);
}
lg(Info, "socket success , _listensock is %d", _listensock);
struct sockaddr_in server;
bzero(&server, sizeof(server));
inet_aton(_ip.c_str(), &server.sin_addr);
server.sin_family = AF_INET;
server.sin_port = htons(_port);
socklen_t len = sizeof(server);
int n = bind(_listensock, (const struct sockaddr *)&server, len);
if (n == -1)
{
lg(Fatal, "bind fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success ,_listensock is %d", _listensock);
int l = listen(_listensock, SOMAXCONN);
if (l == -1)
{
lg(Fatal, "listen fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(Listen_ERR);
}
lg(Info, "listen success ,_listensock is %d", _listensock);
}
void Run()
{
while(true)
{
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &client_len);
if (sockfd == -1)
{
lg(Warning, "accept fail,errno is %d ,error string is %s", errno, strerror(errno));
}
uint16_t clientport = ntohs(client.sin_port);
string clientip = inet_ntoa(client.sin_addr);
lg(Info, "get a new link ,clientip is %s ,clientport is %d,sockfd is %d", clientip.c_str(), clientport, sockfd);
if (sockfd > 0)
{
Service(sockfd, clientip, clientport);
}
}
}
void Service(int sockfd, string &clientip, uint16_t clientport)
{
char buffer[4096];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
cout << "client say #";
buffer[n] = 0;
cout << buffer << endl;
string ret;
ret += "server echo $";
ret += buffer;
write(sockfd, ret.c_str(), ret.size());
}
else if (n == 0)
{
lg(Info, "client quit...,sockfd is %d", sockfd);
break;
}
else
{
lg(Warning, "read fail ,clientip is %s ,clientport is %d,sockfd is %d", clientip.c_str(), clientport, sockfd);
break;
}
}
}
~TcpServer()
{
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
main.cpp
#include <iostream>
#include "tcpserver.hpp"
#include <memory>
using namespace std;
Log lg;
void Usage(string proc)
{
cout << "Usage:\n\r" << proc << "port[1024+]" << endl;
}
int main(int argc,char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
unique_ptr<TcpServer> tcp_svr(new TcpServer());
tcp_svr->InitServer();
tcp_svr->Run();
}
3.5.1代码解析
大体上的逻辑是和udp是一样的只不过tcp面向的是字节流,面向连接,不能直接收发必须得等待客户端来了才能提供服务

- 上面讲解过listen的文件描述符是专门用来把客户端给放进来的
- 至于处理客户端的事情交给的是accept的文件描述符

- read和write和上面的tcp的recv和send在发送的数据时他会自动帮我们将主机序列转成网络序列
- read和write是面向字节流使用的
- 当客户端停止的时候,代表读到了文件结尾,n==0,跳出循环
3.5.2弊端
- 无法处理并发操作从而导致资源zuse
当单进程在处理一个客户端的信息的时候,单进程需要处理完这个客户端的信息才能返回执行下一个客户端的信息
函数都是阻塞等待的
3.6单进程版客户端
#include <iostream>
#include "log.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\r" << proc << "ip" << " port[1024+] " << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
socklen_t server_len = sizeof(server);
if (sockfd == -1)
{
perror("sockfd fail");
exit(1);
}
connect(sockfd, (const sockaddr *)&server, server_len);
char buffer[4096];
while (1)
{
memset(buffer,0,sizeof(buffer));
cout<<"Please Enter #";
string ret;
getline(cin,ret);
write(sockfd,ret.c_str(),ret.size());
int n=read(sockfd,buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]=0;
cout<<buffer<<endl;
}
}
return 0;
}
代码解析

- 客户端需要bind套接字,但是不能我们自己显示的bind端口号,而是由操作系统随机的指派端口号
- 在connect的时候自动bind端口号
3.7多进程版服务端
void Run()
{
while (true)
{
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &client_len);
if (sockfd == -1)
{
lg(Warning, "accept fail,errno is %d ,error string is %s", errno, strerror(errno));
}
uint16_t clientport = ntohs(client.sin_port);
string clientip = inet_ntoa(client.sin_addr);
lg(Info, "get a new link ,clientip is %s ,clientport is %d,sockfd is %d", clientip.c_str(), clientport, sockfd);
// 多进程版
pid_t id = fork();
if (id == 0)
{
close(_listensock);
if(fork()>0) exit(0);
Service(sockfd, clientip, clientport);
close(sockfd);
exit(0);
}
close(sockfd);
pid_t rid=waitpid(id,nullptr,0);
(void)rid;
}
}


- 单进程版只能一个时刻处理一个客户端的服务就相当于餐厅只有一张桌子招待客人
- 我们通过创建子进程的方式让子进程去执行对应的服务
- 子进程会拷贝父进程的文件描述符表,listensock也会继承,但是子进程只需要处理sockfd的事情也就是餐厅客人,不用去接待,同理父进程也是一样
- 父进程要回收子进程,如果子进程没结束父进程就要阻塞等待在那里,还是不能去接收客户端
- 所以可以用非阻塞轮询,这里我们用的是创建孙子进程,子进程退出,父进程立马返回,孙子进程不需要父进程管,被操作系统接受,执行完自动退出
弊端
多进程版创建进程的成本太高不仅要拷贝文件描述符表还要创建tcb创建页表等等,还有占据内存资源已经需要时间开辟管理文件描述符困难等一系列问题
3.8多线程版本服务端
class TcpServer;
class ThreadData
{
public:
ThreadData(int sockfd=defaultsockfd,string ip=defaultip,uint16_t port=defaultport,TcpServer*tcp_svr=nullptr)
:_sockfd(sockfd)
,_port(port)
,_ip(ip)
,_tcp_svr(tcp_svr)
{
}
public:
int _sockfd;
uint16_t _port;
string _ip;
TcpServer* _tcp_svr;
};
static void* Routine(void* args)
{
pthread_detach(pthread_self());
ThreadData* td=static_cast<ThreadData*>(args);
td->_tcp_svr->Service(td->_sockfd,td->_ip,td->_port);
return nullptr;
}
void Run()
{
while (true)
{
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &client_len);
if (sockfd == -1)
{
lg(Warning, "accept fail,errno is %d ,error string is %s", errno, strerror(errno));
}
uint16_t clientport = ntohs(client.sin_port);
string clientip = inet_ntoa(client.sin_addr);
lg(Info, "get a new link ,clientip is %s ,clientport is %d,sockfd is %d", clientip.c_str(), clientport, sockfd);
//多线程版
pthread_t tid;
ThreadData* t=new ThreadData(sockfd,clientip,clientport,this);
pthread_create(&tid,nullptr,Routine,t);
}
}
- 仅改动这些新增一部分主要的代码不变
- 多线程不必关闭文件描述符因为它们看到的都是同一个

- 由于类内的成员函数内有this指针和线程创建函数的参数不匹配所以我们要设置成静态的
- 静态的成员函数只能调用静态的成员方法,但是我们此时采用指针类的形式在线程数据中放置就不用把成员方法设置成静态的就能够直接指向了
- 为了防止使用线程等待阻塞我们采用线程自我分离的形式
弊端
由于每一个客户端来都要创建一个线程来执行对应的任务,而且是来一个任务才创建线程,创建线程也是需要花费时间的
每个客户端都创建线程假设每个服务器都不退就势必会存在着大量的线程占用CPU资源
3.9线程池版本服务端客户端
3.9.1任务
线程池所要执行的任务是一个翻译任务,而这个翻译任务我做的是一次性的服务,也就是说客户端执行完一次翻译任务就要重新连接,以防占据服务器资源导致其他客户端得不到反馈
dict.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unordered_map>
#include <fstream>
#include "log.hpp"
#include <cstdlib>
using namespace std;
extern Log lg;
const string txt = "./dict.txt";
const string d = ":";
static bool Split(string &line, string *part1, string *part2)
{
auto pos = line.find(d);
if (pos == string::npos)
return false;
*part1 = line.substr(0, pos);
*part2 = line.substr(pos + 1);
return true;
}
class Dict
{
public:
Dict()
{
ifstream in(txt);
if (!in.is_open())
{
lg(Fatal, "socket fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(1);
}
string line;
while (getline(in, line))
{
string part1, part2;
Split(line, &part1, &part2);
mp.insert({part1, part2});
}
}
string translate(const string &key)
{
auto pos = mp.find(key);
if (pos == mp.end())
return "find fail";
return pos->second;
}
~Dict()
{
}
private:
unordered_map<string, string> mp;
};
dict.txt存放的是一些kv式的中英文对照比如apple:苹果
task.hpp
#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "dict.hpp"
#include <unistd.h>
using namespace std;
extern Log lg;
Dict dict;
class Task
{
public:
Task()
{
}
Task(int sockfd, string &clientip, uint16_t clientport)
: _sockfd(sockfd), _clientip(clientip), _clientport(clientport)
{
}
void Run()
{
char buffer[4096];
memset(buffer,0,sizeof(buffer));
ssize_t n = read(_sockfd, buffer, sizeof(buffer)-1);
if (n > 0)
{
buffer[n]=0;
cout << "client say #"<<buffer<<endl;
string ret=dict.translate(buffer);
int wr=write(_sockfd, ret.c_str(), ret.size());
if(wr<0)
{
lg(Warning, "write fail,errno is %d ,error string is %s", errno, strerror(errno));
}
}
else if (n == 0)
{
lg(Info, "client quit...,sockfd is %d", _sockfd);
}
else
{
lg(Warning, "read fail ,clientip is %s ,clientport is %d,sockfd is %d", _clientip.c_str(), _clientport, _sockfd);
}
close(_sockfd);
}
void operator()()
{
Run();
}
private:
int _sockfd;
string _clientip;
uint16_t _clientport;
};
3.9.2线程池
线程池的讲解在linux线程的我的上一篇博客当中有讲解这里我就直接贴出来
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <queue>
using namespace std;
struct ThreadData
{
pthread_t tid;
string name;
};
const int defaultnum = 5;
template <class T>
class Threadpool
{
private:
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void UnLock()
{
pthread_mutex_unlock(&_mutex);
}
void Wakeup()
{
pthread_cond_signal(&_cond);
}
void tSleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
bool isemp()
{
return _q.empty();
}
string GetThreadname(pthread_t tid)
{
for (auto &ti : _td)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
static void *HandlerTask(void *args)
{
Threadpool<T> *tp = static_cast<Threadpool<T> *>(args);
string name = tp->GetThreadname(pthread_self());
while (1)
{
// 检测是否有任务
tp->Lock();
while (tp->isemp())
{
tp->tSleep();
}
T t = tp->Pop();
tp->UnLock();
t();
// cout << name << " 生产者生产了一个任务 " << t.Getanswer() << endl;
}
}
T Pop()
{
T t = _q.front();
_q.pop();
return t;
}
void Start()
{
int num = _td.size();
for (int i = 0; i < num; i++)
{
_td[i].name = "Thread-" + to_string(i + 1);
pthread_create(&(_td[i].tid), nullptr, HandlerTask, this);
}
}
// 向外界提供Push接口向线程池推送任务
void Push(const T &in)
{
Lock();
_q.push(in);
// 唤醒线程来执行任务
Wakeup();
UnLock();
}
static Threadpool<T> *GetInstance(int num = defaultnum)
{
if (_tp == nullptr)
{
pthread_mutex_lock(&_lock);
if (_tp == nullptr)
{
_tp = new Threadpool<T>(num);
}
pthread_mutex_unlock(&_lock);
}
return _tp;
}
private:
Threadpool(int maxl = defaultnum)
: _maxl(maxl), _td(maxl)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
~Threadpool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
Threadpool(const Threadpool<T> &t) = delete;
const Threadpool<T> &operator=(const Threadpool<T> &t) = delete;
static Threadpool<T> *_tp;
static pthread_mutex_t _lock;
private:
vector<ThreadData> _td;
queue<T> _q;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
int _maxl;
};
template <class T>
Threadpool<T> *Threadpool<T>::_tp = nullptr;
template <class T>
pthread_mutex_t Threadpool<T>::_lock = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
3.9.3服务端
#pragma once
#include <iostream>
#include "log.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include "task.hpp"
#include <signal.h>
using namespace std;
static const int defaultsockfd = -1;
static const uint16_t defaultport = 8081;
static const string defaultip = "0.0.0.0";
extern Log lg;
enum
{
SOCK_ERR = 1,
BIND_ERR,
Listen_ERR,
};
class TcpServer
{
public:
TcpServer(const uint16_t &port = defaultport, const string &ip = defaultip)
: _listensock(defaultsockfd), _port(port), _ip(ip)
{
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
lg(Fatal, "socket fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(SOCK_ERR);
}
lg(Info, "socket success , _listensock is %d", _listensock);
struct sockaddr_in server;
bzero(&server, sizeof(server));
inet_aton(_ip.c_str(), &server.sin_addr);
server.sin_family = AF_INET;
server.sin_port = htons(_port);
socklen_t len = sizeof(server);
int n = bind(_listensock, (const struct sockaddr *)&server, len);
if (n == -1)
{
lg(Fatal, "bind fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success ,_listensock is %d", _listensock);
int l = listen(_listensock, SOMAXCONN);
if (l == -1)
{
lg(Fatal, "listen fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(Listen_ERR);
}
lg(Info, "listen success ,_listensock is %d", _listensock);
}
void Run()
{
signal(SIGPIPE,SIG_IGN);
Threadpool<Task>::GetInstance()->Start();
while (true)
{
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int sockfd = accept(_listensock, (struct sockaddr *)&client, &client_len);
if (sockfd == -1)
{
lg(Warning, "accept fail,errno is %d ,error string is %s", errno, strerror(errno));
}
uint16_t clientport = ntohs(client.sin_port);
string clientip = inet_ntoa(client.sin_addr);
lg(Info, "get a new link ,clientip is %s ,clientport is %d,sockfd is %d", clientip.c_str(), clientport, sockfd);
//进程池版本
Task t(sockfd,clientip,clientport);
Threadpool<Task>::GetInstance()->Push(t);
}
}
~TcpServer()
{
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
3.9.4客户端
#include <iostream>
#include "log.hpp"
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
using namespace std;
Log lg;
enum
{
SOCK_ERR = 1,
};
void Usage(string proc)
{
cout << "Usage:\n\r" << proc << "ip" << " port[1024+] " << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
struct sockaddr_in server;
socklen_t server_len = sizeof(server);
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
while (true)
{
int cnt = 6;
bool isconnect = false;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
lg(Fatal, "socket fail,errno is %d ,error string is %s", errno, strerror(errno));
exit(SOCK_ERR);
}
do
{
int con = connect(sockfd, (const sockaddr *)&server, server_len);
if (con < 0)
{
cnt--;
cout << "connet err ,is reconnecting :" << cnt << endl;
sleep(1);
}
else
{
isconnect = true;
cout << "connet success "<< endl;
}
} while (cnt && !isconnect);
if (cnt == 0)
{
lg(Warning, "connect fail,errno is %d ,error string is %s", errno, strerror(errno));
break;
}
char buffer[4096];
memset(buffer, 0, sizeof(buffer));
cout << "Please Enter #";
string ret;
getline(cin, ret);
int w = write(sockfd, ret.c_str(), ret.size());
if (w < 0)
{
lg(Warning, "write fail,errno is %d ,error string is %s", errno, strerror(errno));
}
int n = read(sockfd, buffer, sizeof(buffer));
if (n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
close(sockfd);
}
return 0;
}
3.9.5代码解析

- 这个是单例版本的线程池,当服务端将客户端的任务投递到线程池当中去执行任务后就继续去获取新的连接
- 当执行完一次任务后服务器就释放了客户端有关的套接字信息


- 客户端如果像错误的文件描述符当中写入就会写入失败
- 如果客户端把文件描述符关闭的话,服务端就会写入失败,操作系统会向服务端发送信号杀掉服务端,为了防止这种情况,我们需要将信号的捕捉动作改为忽略

- 客户端进行一次性服务后还想进行服务的话就要重新建立连接,然而建立连接是有可能发生失败的
- 套接字的初始信息我们都已经预先设置好不用重复初始化就放在循环外面
- 像我们平时打游戏的时候网不好就有可能和服务器断开连接这个时候客户端显示器上就会显示正在连接中
- 当连接失败的时候我们就会进行循环判断是否重新连接成功,连接成功此时cnt非0为真,isconnect为false为假条件不成立跳出循环
- 当执行完一次任务后,如果服务器断开连接写入失败,回到循环开始判断连接失败重新连接
四、守护进程
- 每个账户登录的时候操作系统都会为其形成一份会话(session)
- 每一个会话都会启动一个bash进程为用户提供命令行服务
- 在一个会话(session)里面,只能存在一个前台进程(能获取键盘输入),可以存在多个后台进程
- 当我们将后台进程[./[可执行程序] &]放到前台进程的时候,bash就自动变为后台进程了
- 后台进程如果一直向显示器上输出就会影响我们的正常输入输出,所以我们一般将后台进程打印的消息重定向到文件当中
4.1相关操作后台前台进程
[wyx@hcss-ecs-000a tcp_socket]$ ./process >> test.txt &
[1] 12847
把任务放在后台默认会形成这样的一个数字,这个数字叫做后台任务号
[wyx@hcss-ecs-000a tcp_socket]$ jobs
[1] Running ./process >> test.txt &
[2]- Running ./process >> test.txt &
[3]+ Running ./process >> test.txt &
使用jobs指令就能查看后台任务
[wyx@hcss-ecs-000a tcp_socket]$ fg 2
./process >> test.txt
^C
[wyx@hcss-ecs-000a tcp_socket]$ jobs
[1]- Running ./process >> test.txt &
[3]+ Running ./process >> test.txt &
使用fg就能将对应的后台进程提到前台而前台进程就能ctrl+c杀掉了
[wyx@hcss-ecs-000a tcp_socket]$ jobs
[1]- Running ./process >> test.txt &
[3]+ Running ./process >> test.txt &
[wyx@hcss-ecs-000a tcp_socket]$ fg 1
./process >> test.txt
^Z
[1]+ Stopped ./process >> test.txt
[wyx@hcss-ecs-000a tcp_socket]$ ls
ctrl+z可以向前台进程发送暂停的信号,此时前台进程什么都不做就自动变成后台,而bash就自动变成前台了
[wyx@hcss-ecs-000a tcp_socket]$ bg 1
[1]+ ./process >> test.txt &
[wyx@hcss-ecs-000a tcp_socket]$ jobs
[1]- Running ./process >> test.txt &
[3]+ Running ./process >> test.txt &
想让后台暂停的进程重新启动就叫做bg指令
4.2进程组
[wyx@hcss-ecs-000a tcp_socket]$ jobs
[1]- Running ./process > test.txt &
[2]+ Running sleep 1000 | sleep 2000 | sleep 3000 &
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12660 13212 13212 12660 pts/4 13339 S 1000 0:00 ./process
12660 13278 13278 12660 pts/4 13339 S 1000 0:00 sleep 1000
12660 13279 13278 12660 pts/4 13339 S 1000 0:00 sleep 2000
12660 13280 13278 12660 pts/4 13339 S 1000 0:00 sleep 3000
在当前的会话里我们启了两个后台任务
- PGID:进程组ID
- SID:会话(session)ID
- 单起的进程process自成一个进程组,而sleep的PGID都一样组成一组,组成是多个进程的第一个,进程组ID就是组长的PID
- 任务是要由进程组来完成的,可以是一个进程也可以是多个进程
- 所以前台进程后台进程也可以叫做前台任务和后台任务
- 多个任务都是在同一个session(以bash的ID命名构建一个session)内启动的,SID是一样的
4.3守护进程化

- 我们一旦将会话关掉再重新启动就跟终端没关系了,PPID都变成1了,会话和进程还是保留的---收到了用户登录和退出的影响的
- 所以我们想做一个不想收到任何用户登录注册的影响--守护进程化

- 让进程自成一个会话,不需要和键盘和显示器产生关联
- 自成进程组自称会话的进程,称为守护进程
4.4setsid函数
#include <unistd.h>
pid_t setsid(void);
- 谁调用这个函数就创建一个会话,把这个进程的组ID设置成这个会话的ID
- 创建一个新会话,调用进程不能是进程组的组长
- 所以我们采用fork创建一个进程,父进程退出,子进程调用setsid子进程就自成会话了
- 守护进程的本质也是孤儿进程
4.5代码演示
Demon.hpp
#include <iostream>
#include <unistd.h>
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string defaultfile="/dev/null";
void Demon(const std::string &pathname = "")
{
// 1.忽略异常信号
signal(SIGCLD, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2.将进程变成一个独立会话
if (fork() > 0)
exit(0);
setsid();
// 3.更改当前工作目录
if (!pathname.empty())
chdir(pathname.c_str());
// 4.将标准输入标准输出标准错误重定向到"垃圾桶中"
int fd=open(defaultfile.c_str(),O_RDWR);
if(fd>0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
}
/dev/null 是 Linux/Unix 系统中的空设备文件
- 位置:/dev/null
- 类型:字符设备文件
- 作用:丢弃所有写入的数据,读取时立即返回EOF
守护进程将数据重定向到空设备文件当中,就是防止cout和cin的来影响输入输出,至于日志可以重定向到文件当中
- 不依赖任何终端
- 不产生任何屏幕输出
- 独立运行在后台

当我们添加守护进程到代码当中后运行就可以变成守护进程了


我们就可以向一个不会终止的进程发送消息了随时随地
4.6daemon函数
#include <unistd.h>
int daemon(int nochdir, int noclose);
我们也可以用这个函数来达到守护进程化的目的
- nochdir:0表示工作目录设置为根目录下,否则就使用当前工作目录
- noclose:0表示把标准输入输出错误重定向到/dev/null,否则就不动