中国建筑服务信息网,洛阳网站优化,如何做关于橱柜网站,淘客网站开发教程文章目录 一、再识网络1. 端口号2. 网络字节序列3.TCP 与 UDP 二、套接字1.sockaddr结构2.UDP1.server端1.1 构造函数1.2 Init1.3 Run 2.客户端1.Linux2.Windows 3.TCP1. 基本接口2. 客户端3. 服务端1.版本12.版本23.版本34.版本4 三、守护进程尾序 温馨提示#xff1a;文章较… 文章目录 一、再识网络1. 端口号2. 网络字节序列3.TCP 与 UDP 二、套接字1.sockaddr结构2.UDP1.server端1.1 构造函数1.2 Init1.3 Run 2.客户端1.Linux2.Windows 3.TCP1. 基本接口2. 客户端3. 服务端1.版本12.版本23.版本34.版本4 三、守护进程尾序 温馨提示文章较长(代码较多)请收藏起来慢慢观看。 一、再识网络
1. 端口号 在上文我们大致理解了网络传输的基本流程和相关概念知道ip地址可以标识唯一的一台主机但是主机获取到信息的最终目的是为了呈现给上层的用户即我们所熟知的抖音等APP既然有很多的APP具体给哪一个APP呢 说明 APP具体指的是运行起来的程序即一个一个的进程。网络通信的本质是进程之间借助网卡(共享资源)进行通信。 概念 端口号 指的是用于标记进程或者服务的逻辑地址。范围为0 到 65535分有大致三类 系统端口系统端口范围是从 0 到 1023这些端口通常被一些众所周知的网络服务占用比如 HTTP端口 80、HTTPS端口 443、FTP端口 21、SSH端口 22等。通常需要root权限才能够进行使用。注册端口注册端口范围是从 1024 到 49151这一范围的端口通常被一些应用程序或服务占用。普通用户也可进行使用。动态/私有端口动态/私有端口范围是从 49152 到 65535也被称为私有端口或暂时端口。这些端口通常被客户端应用程序使用用于临时通信。 疑问既然进程的pid能标识唯一的进程那为什么不直接捡现成的用呢
答 进程pid VS 端口号:
从概念上看 pid操作系统管理进程使用。端口号网络通信以及为应用程序提供服务。两者实现的解耦合的关系。 从使用形式上看 pid: 进程创建时才拥有。端口号固定一段范围0 到 65535。 从用法来看 pid: 一个进程只能有一个pid。端口号一个进程能有多个端口号为用户提供不同的服务。联系pid和端口号都是只能对应一个进程。且通过端口号可找到进程pid从而找到进程。 总结 通过IP地址标识唯一的一台主机。通过端口号标识唯一的一个进程。进而我们可以实现网络之间的通信。 拓展在实际进行通信的过程中一般是由客户端访问服务器由服务器再提供对应的服务。 说明 客户端要想访问服务器首先得知道服务器的ip地址和对应服务的端口号。这些工作早已经由开发人员做好因此无需担心。服务器的ip地址和端口号一般是不能发生变化的否则客户端就无法访问。因为客户端的载入的服务器的端口号和ip一般是固定的。客户端的端口号是动态变化的。这是因为多个app的开发厂商并不互通因此可能存在端口号冲突的现象因此要动态绑定端口号而且这样做更加灵活安全高效。服务器要对大量用户提供服务而且用户的IP地址是随机变化的这也间接的导致了服务器要在客户端做一些手脚, 即固定服务器的ip地址和端口号。 2. 网络字节序列 关于数据用大端还是用小端就跟鸡蛋先吃大头还是先吃小头一样没有实际的争论意义因此我们看到电脑既有大端机也有小端机。 说明big - endian 为大端机的数据little - endian为小端机的数据。速记大同小异反着记——大 “异” 小 “同”。 但是网络里面传输数据不可能即传输大端数据也传输小端因此规定统一在网络里面传输大端数据到对应的主机里面再进行统一的转换大端不用变小端再转换一下即可。 相关的接口
#include arpa/inet.huint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);速记h(host) to n(net) l(long)即将long类型的数据从主机转网络。其余类似。 3.TCP 与 UDP
传输方式
TCP面向字节流。UDP面向数据报。
这是最本质的差别下面我们就这两点进行分析 面向字节流 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送接收方按照接收到的顺序重新组装数据。提供了可靠的传输保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。基于连接的通信方式需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销但可以确保数据的有序传输。适用于需要可靠传输和有序性的应用如文件传输、视频流传输等。 总结TCP协议面向字节流因此可靠有连接。适合文件和视频等信息的传输。 面向数据报 将数据划分为独立的数据即数据报每个数据报都携带了完整的信息可以独立地发送和接收。不保证数据的可靠性每个数据报都是独立传输的可能会发生丢失、重复或乱序。无连接的通信方式每个数据报都是独立的不需要事先建立连接。对实时性要求较高的应用如实时音频、视频通信等因为它可以提供更低的延迟。 总结UDP协议面向数据报因此不可靠无连接。适用于对实时性要求高的应用。 说明这里的可靠和不可靠是一个中性词。不可靠意味着较低的成本实现更加简单可靠意味着实现需要较大的代价。因此没有谁好谁坏。 下面我们实现是更为简单的UDP套接字。 在开始之前我们先来解决一个前置问题主要是服务器的端口问题一般默认有些端口是禁掉的不能用于网络之间的通信因此我们需要开放一些端口供我们之间通信使用。实现步骤 登录所在云服务的官网。(我的是阿里云的)点击控制台。点击云服务器ESC/轻量级服务器/云服务器找到对应的云服务器。(我的是轻量级云服务器)如果是云服务器ESC/服务器就找到安全组点击安全组ID进行编辑即可。如果是轻量级服务器就在服务器一栏找到实例id点击再点击防火墙进行编辑即可。 具体步骤——阿里云轻量级云服务器 第一步 第二步 第三步 第四步 二、套接字
1.sockaddr结构
这是一层抽象化的结构设计之初是为了统一网络套接字的接口使用是一套通用的网络套接字而对应的具体的套接字有 网络套接字 与 域间套接字。
图解 类似多态的思想即从抽象到具体。在使用过程中我们可以通过传入通用的套接字类型并且指定对应的套接字大小从而说明其对应的具体类型也就是我们说的多态。 我们实现的是网络编程使用的是struct sockaddr_in。 具体结构 sin_family_t sin_family; 所属家族协议类型一般设置为AF_INT/PF_INT即ipv4类型的协议。in_port_t sin_port; 端口号。struct in_addr sin_addr; ip地址。 注意端口号和ip地址的数据都为网络序列。 2.UDP
Log.hpp(记录日志信息)
#pragma once
#includemap
#includeiostream
#includecstdio
#includestdarg.h
#includeunistd.h
#includesys/types.h
#includesys/stat.h
#includefcntl.h
#includetime.h
using namespace std;
#define SIZE (4096)//事件的等级
#define EMRGE 1
#define ALERK 2
#define CRIT 3
#define ERRO 4
#define WARNNING 5
#define NOTICE 6
#define INFORE 7
#define DEBUG 8
#define NONE 9//输出方向
#define DEFAULTFILE 1
#define CLASSFILE 2
#define SCREAN 0
//说明一般我们在传参时一般都是以宏的方式进行传参的
//如果需要打印出字符串可以用KV类型进行映射转换。
mapint,string Mode {{1,EMERG},{2,ALERK},{3,CRIT},{4,ERRO},{5,WARNING},{6,NOTICE},{7,INFOR},{8,DEBUG},{9,NONE}
};//分类文件处理的后缀。
mapint,string file {{1,emerg},{2,alerk},{3,crit},{4,erro},{5,warning},{6,notice},{7,infor},{8,debug},{9,none}
};
class Log
{
public:void operator()(int level,const char* format,...){//将输入的字符串信息进行输出。va_list arg;va_start(arg,format);char buf[SIZE];vsnprintf(buf,SIZE,format,arg);va_end(arg);//获取时间time_t date time(NULL);struct tm* t localtime((const time_t *)date);char cur_time[SIZE] {0};snprintf(cur_time,SIZE,[%d-%d-%d %d:%d:%d],\t-tm_year 1900,t-tm_mon 1,t-tm_mday,t-tm_hour,t-tm_min,t-tm_sec);//输入再进行合并string Log [ Mode[level] ] \cur_time string(buf) \n;//处理输出方向PrintClassFile(level,where,Log);}void PrintDefaultFILE(string file_name,const string mes){int fd open(file_name.c_str(),O_CREAT | O_WRONLY \| O_APPEND,0666);write(fd,mes.c_str(),mes.size());close(fd);}//将文件进行分类进行输出。void PrintClassFile(int level,int where,const string mes){if(where SCREAN)cout mes;else{string file_name ./log.txt;if(where CLASSFILE)file_name (. file[level]);PrintDefaultFILE(file_name,mes);}}void ReDirect(int wh){where wh;}
private:int where SCREAN;
};说明在【Linux进阶之路】进程间通信有所提及具体这个小组件是用来帮助我们显示出日志的时间等级出错内容等信息。
1.server端
基本框架
//所用容器
#includestring
#includeunordered_map//与内存相关的头文件
#includestring.h
#includestrings.h//网络相关的头文件
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h //包装器
#includefunctional//日志头文件
#include Log.hpp//枚举常量用于失败退出进程的退出码
enum
{SOCKET_CREAT_FAIL 1,SOCKET_BIND_FAIL,
};
class UdpServer
{
public:UdpServer(uint16_t port,string ip):_port(port),_ip(ip),_sockfd(0){}~UdpServer(){}void Init(){}void Run(){}
private:int _sockfd;string _ip;uint16_t _port;
};1.1 构造函数 一般我们使用1024以上的端口号即可此处我们默认使用8080端口。云服务器禁止直接绑定公网ip。 . 解决方法——因此我们绑定的时候使用0.0.0.0即任意ip地址绑定即可即接收所有云服务器地址的发来的信息。. 方法优点——服务可以在服务器上的所有IP地址和网络接口上进行监听从而提供更广泛的访问范围。 因此在构造函数里我们给出两个缺省值即可。
//全局定义
uint16_t default_port 8080;
string default_string 0.0.0.0;//类内
UdpServer(uint16_t port default_port,string ip default_string)
:_port(port),_ip(ip),_sockfd(0)
{}1.2 Init
创建套接字 接口 //头文件
#include sys/types.h
#include sys/socket.h
//函数声明
int socket(int domain, int type, int protocol);
/*
参数1指定通信域,使用AF_INT即可即IPV4的ip地址。2: SOCKET_DGRAM,即使用的套接字类型指的是UDP类型的套接字。3: 指定协议一般设为0根据前两个参数系统会自动选择合适的协议。
返回值1.成功返回对应的文件描述符,网络对应的是网卡文件。2.失败返回-1。*/绑定套接字 接口 //头文件
#include sys/types.h
#include sys/socket.h
//函数声明
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);/*
参数1.网络的文件描述符。2.sockaddr具体对象对应的地址为输入型参数。3.具体对象对应的大小为输入型参数。
说明在传参之前sockaddr对象应初始化完成。返回值1.成功返回 0。2.失败返回 -1并设置合适的错误码。
*/说明在传入sockaddr具体对象对应的地址时需要再强转为sockaddr*类型的因为也传进去了具体对象的大小所以内部会再识别出具体的对象再进行处理。 这里的IP地址的形式为字符串类型的便于用户进行识别而在网络当中是usiged int 类型的中间需要转换一下。 实现代码
#includestring
#includeiostream
using std::string;
using std::cout;
using std::endl;struct StrToIp
{unsigned int str_to_ip(const string str){int ssz str.size();int begin 0;int index 3;for (int i 0; i ssz; i){if (str[i] . || i ssz){string tmp str.substr(begin,i);begin i 1;unsigned char n stoi(tmp);if (index 0) return 0;part[index--] n;}}//auto p (unsigned char*)ip;//for (int i 0; i 4; i)//{// *(p i) part[i];//}//return ip;return *((unsigned int*)part);}unsigned char part[4] { 0 };//unsigned int ip 0;};int main()
{StrToIp s;cout s.str_to_ip(59.110.171.160) endl;return 0;
}我们将字符串分为四部分然后转换为char类型的四个变量存储即可。这四个部分我们存放在数组或者单独存都可以这里我采用数组便于操作。如果为数组具体转换为int变量时应注意四个部分的存储顺序。 运行结果 说明 我所在的机器为小端机数据是低位放在低地址处所以应该倒着存每一段。如果为大端机数据是高位放在低地址处所以应该正着存每一段。最后强转取数据即可。 补充 指针指向的是对象的低地址处。 在实际编程的过程中相应的接口已经准备好不需要手动的写但相应的原理还是要清楚的。 字符串转地址的网络序列接口 //头文件
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.hint inet_aton(const char *cp, struct in_addr *inp);/*
参数1.转化的ip地址的字符串。2.输出型参数in_addr的变量。
返回值1.成功返回非零值通常为1.2.失败返回零值。
*/
in_addr_t inet_addr(const char *cp);
/*
参数1.转化的ip地址的字符串。
返回值1.成功返回对应的ip值。2.失败返回INADDR_NONE其定义为 (in_addr_t) -1。
*/ 主机ip地址转字符串的接口 //头文件
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
char *inet_ntoa(struct in_addr in);/*参数存放主机序列的ip地址
返回值字符串形式的ip。*/实现代码 void Init(){//1.创建套接字即创建文件描述符_sockfd socket(AF_INET,SOCK_DGRAM,0);if(_sockfd 0){lg(CRIT,socket fail,error message is %s,error \number is %d ,strerror(errno),errno);exit(SOCKET_CREAT_FAIL);}lg(INFORE,socket fd is %d,socket success!,_sockfd);//2.绑定套接字/*注意主机序列都要转成网络序列。*/// 2.1初始化域间套接字struct sockaddr_in server_mes;bzero(server_mes,sizeof(server_mes));server_mes.sin_family AF_INET;server_mes.sin_port htons(_port); server_mes.sin_addr.s_addr inet_addr(_ip.c_str());socklen_t len sizeof(server_mes);// server_mes.sin_addr.s_addr INADDR_ANY;; //任意地址转网络序列// 2.2 绑定域间套接字int ret bind(_sockfd,(const sockaddr*)server_mes,len);if(ret 0){lg(CRIT,bind fail,error message is %s,\error number is %d ,strerror(errno),errno);exit(SOCKET_BIND_FAIL);}lg(INFORE,ret is %d,bind success!,ret);}1.3 Run
等待客户发信息 接口 //头文件
#include sys/types.h
#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);/*
参数:1.文件描述符。2.缓存区读取用户发来的信息。3.缓存区的大小。4.一般使用默认值0即可。5.src_addr变量的地址用于接收用户的网络信息,输出型参数。6.addrlen用于接受用户的src_addr具体对象的长度输入输出型参数。返回值1.成功返回接受的字节个数。2.连接关闭返回0。3.错误返回-1.设置合适的错误码。
*/
给客户提供服务 接口 //头文件:
#include sys/types.h
#include sys/socket.hssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)
参数:1.文件描述符。2.缓存区存放发给用户的信息。3.缓存区的大小。4.一般使用默认值0即可。5.src_addr变量的地址用于存放用户的网络信息。6.addrlen用于存放用户的src_addr具体对象的长度。返回值1.成功返回实际发送的字节个数。3.错误返回-1.设置合适的错误码。实现代码 void Run(){for(;;){//存放用户消息的缓存区char buffer[1024] {0};//用于存放用户的网络信息struct sockaddr_in client_mes; socklen_t len;//收消息ssize_t n recvfrom(_sockfd,buffer,sizeof(buffer) - 1\,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,recvfrom message fail,waring \message is %s,strerror(errno));continue;}buffer[n] 0;//uint_32_t 转 stringstring cip inet_ntoa(client_mes.sin_addr);uint16_t cport ntohs(client_mes.sin_port);string echo_mes [ cip : to_string(cport)\ ] buffer;cout echo_mes endl;//发消息n sendto(_sockfd,echo_mes.c_str(),echo_mes.size(),\0,(sockaddr*)client_mes,len);}}这里只是简单的使用接口因此完成收发消息即可。 server.hpp
#pragma once
//所用容器
#includestring
#includeunordered_map//与内存相关的头文件
#includestring.h
#includestrings.h//网络相关的头文件
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h //包装器
#includefunctional//日志头文件
#include Log.hpp
//枚举常量用于失败退出进程的退出码
enum
{SOCKET_CREAT_FAIL 1,SOCKET_BIND_FAIL,
};Log lg;
uint16_t default_port 8888;
string default_string 0.0.0.0;
class UdpServer
{
public:UdpServer(uint16_t port default_port,string ip \ default_string):_port(port),_ip(ip),_sockfd(0){}~UdpServer(){if(_sockfd 0) {close(_sockfd);}}void Init(){//1.创建套接字即创建文件描述符_sockfd socket(AF_INET,SOCK_DGRAM,0);if(_sockfd 0){lg(CRIT,socket fail,error message is %s,error \number is %d ,strerror(errno),errno);exit(SOCKET_CREAT_FAIL);}lg(INFORE,socket fd is %d,socket success!,_sockfd);//2.绑定套接字/*注意1.主机序列转成网络序列。*/struct sockaddr_in server_mes;bzero(server_mes,sizeof(server_mes));server_mes.sin_family AF_INET;server_mes.sin_port htons(_port); server_mes.sin_addr.s_addr inet_addr(_ip.c_str());socklen_t len sizeof(server_mes);// server_mes.sin_addr.s_addr INADDR_ANY;; //任意地址转网络序列int ret bind(_sockfd,(const sockaddr*)server_mes,len);if(ret 0){lg(CRIT,bind fail,error message is %s,error number is\%d ,strerror(errno),errno);exit(SOCKET_BIND_FAIL);}lg(INFORE,ret is %d,bind success!,ret);}void Run(){for(;;){char buffer[1024] {0};struct sockaddr_in client_mes; //用户的网络信息socklen_t len sizeof(client_mes);//收消息ssize_t n recvfrom(_sockfd,buffer,sizeof(buffer) - 1\,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,recvfrom message fail,waring message \is %s,strerror(errno));continue;}buffer[n] 0;string cip inet_ntoa(client_mes.sin_addr);uint16_t cport ntohs(client_mes.sin_port);string echo_mes [ cip : to_string(cport)\ ] buffer;cout echo_mes endl;//发消息n sendto(_sockfd,echo_mes.c_str(),echo_mes.size()\,0,(sockaddr*)client_mes,len);}}
private:int _sockfd;string _ip;uint16_t _port;
};server.cc
#includeiostream
#includevector
#includeudpserver.hpp
void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 2){Usage(argv[0]);return 1;}uint16_t port stoi(argv[1]);UdpServer* ser new UdpServer(port);ser-Init();ser-Run();return 0;
}当服务器启动成功时我们可以使用 netstat -naup 查看对应的服务器。说明-n表示 net-a 表示 all-u 表示 udp, -p 表示 process, 即显示出所有的udp套接字的信息。图解 2.客户端
1.Linux 实际上只有四步 创建网络套接字。 输入要发送的消息。 发消息。 收消息。 注意 在客户端我们并不需要主动bind端口号而是应该由系统自动分配端口号这样即避免了不同应用程序之间端口号的冲突也变向的提高了安全性灵活性。端口号在调用sento函数时自动进行绑定。 client.hpp
#pragma once
//容器
#includestring//内容接口
#includestring.h
#includestrings.h//网络相关的接口
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h //线程
#includepthread.h
//日志
#include Log.hpp
using std::string;
Log lg;
enum
{SOCKET_CREAT_FAIL 1,SOCKET_BIND_FAIL,
};
string default_ip 59.110.171.164;
uint16_t default_port 8888;
struct UdpClient
{
public:UdpClient(uint16_t port default_port,string ip default_ip):_ip(ip),_port(port){}~UdpClient(){if(_sockfd 0) {close(_sockfd);}}void Init(){_sockfd socket(AF_INET,SOCK_DGRAM,0);if(_sockfd 0){lg(CRIT,socket create fail,error message is %s,error \is %d,strerror(errno),errno);exit(SOCKET_CREAT_FAIL);}lg(INFORE,socket create success, socketfd is %d,_sockfd);//在发送消息的时候会自动进行绑定。}void Run(){struct sockaddr_in server_mes;bzero(server_mes,sizeof(server_mes));server_mes.sin_addr.s_addr inet_addr(_ip.c_str());server_mes.sin_family AF_INET;server_mes.sin_port htons(_port);socklen_t len sizeof(server_mes);while(true){string str;cout please enter;getline(cin,str);ssize_t n sendto(_sockfd,str.c_str(),str.size(),0,\(sockaddr*)server_mes,len);if(n 0){lg(WARNNING,send message fail,error message is \%s,error is %d,strerror(errno),errno);continue;}char buffer[SIZE] {0};n recvfrom(_sockfd,buffer,SIZE - 1,0,\(sockaddr*)server_mes,len);buffer[n] \0;cout buffer endl;}}int _sockfd;string _ip;uint16_t _port;
};client.cc
#include udpclient.hpp
void Usage(char* pragma_name)
{cout endl Usage: pragma_name ip \ port[8080-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 3){Usage(argv[0]);return 1;}string ip argv[1];uint16_t port stoi(argv[2]);UdpClient* client new UdpClient(port,ip);client-Init();client-Run();return 0;
}示例
2.Windows
WindowsClient.hpp
//定义此宏是为了屏蔽inet_addr这个错误。
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1#includeiostream
#includestring#includeWinSock2.h
//此头文件应该包含于Windows.h之上可能的原因是重复包含相同的声明
//也就是没有写#pragma once之类的。#includeWindows.h#includecstdlib
#includecstring#pragma comment(lib,ws2_32.lib)//包含一个库using std::string;
using std::cin;
using std::cout;
using std::endl;enum
{START_FAIL 1,SOCKET_FAIL 2,
};string default_ip 59.110.171.164;
uint16_t default_port 8080;struct UdpClient
{
public:UdpClient(uint16_t port default_port, string ip default_ip):_ip(ip), _port(port),_sockfd(0){WSADATA wsd;int ret WSAStartup(MAKEWORD(2, 2), wsd);if (ret ! 0){perror(WSAStartup);exit(START_FAIL);}}~UdpClient(){if(_sockfd 0) {close(_sockfd);}WSACleanup();}void Init(){_sockfd socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd 0){perror(socket);exit(SOCKET_FAIL);}}void Run(){struct sockaddr_in server_mes;server_mes.sin_addr.S_un.S_addr inet_addr(_ip.c_str());server_mes.sin_port htons(_port);server_mes.sin_family AF_INET;int len sizeof(server_mes);while (true){string str;cout please enter;getline(cin, str);int ret sendto(_sockfd, str.c_str(), str.size(), \0, (const sockaddr*)server_mes, len);if (ret 0){cout ret endl;perror(sento);continue;}char buffer[1024] { 0 };int size recvfrom(_sockfd, buffer, \sizeof(buffer) - 1, 0, (sockaddr*)server_mes, len);if (size 0){perror(recvfrom);continue;}buffer[size] \0;cout buffer endl;}}int _sockfd;string _ip;uint16_t _port;
};Client.cc
#define _CRT_SECURE_NO_WARNINGS 1
#includeWindowsClient.hpp
//智能指针所在头文件
#includememory
int main()
{//使用unique_ptr确保只有一台服务器。std::unique_ptrUdpClient cp(new UdpClient());cp-Init();cp-Run();return 0;
}Linux与Windows的代码相比之下,其实就构造和析构多了一点东西其余基本完全相同。 上述实现代码我们称之为版本1。 基本的收发消息我们是可以完成的而且我们通过端口号和ip标识了唯一的一台主机的唯一进程这样我们可以基于此简单的实现一个基于网络的聊天室:
客户端 收消息和发消息应该用不同的线程进行执行因此我们需要在客户端创建两个线程收消息和发消息。具体的实现操作我们可分为两步 将收消息的线程输出到错误流中。在运行时将错误流再重定向到指定的终端文件当中。 查看终端文件ls /dev/pts 客户端更改代码 static void* SendMessage(void* args){auto threadptr static_castpairUdpClient*,\struct sockaddr_in**(args);UdpClient* cptr threadptr-first;sockaddr* sptr (sockaddr*)threadptr-second;//发消息while(true){string str;cout please enter;getline(cin,str);ssize_t ret sendto(cptr-_sockfd,str.c_str()\,str.size(),0,sptr,sizeof(sockaddr_in));if(ret 0){lg(WARNNING,send message fail,error message\is %s,error is %d,strerror(errno),errno);continue;}} }static void* ReceiveMessage(void* args){auto threadptr static_castpairUdpClient*,\struct sockaddr_in**(args);UdpClient* cptr threadptr-first;sockaddr* sptr (sockaddr*)threadptr-second;socklen_t len sizeof(sockaddr_in);//收消息while(true){char buffer[SIZE] {0};int ret recvfrom(cptr-_sockfd,buffer,SIZE - 1,\0,sptr,len);cerr buffer endl;}}void Run(){struct sockaddr_in server_mes;bzero(server_mes,sizeof(server_mes));server_mes.sin_addr.s_addr inet_addr(_ip.c_str());server_mes.sin_family AF_INET;server_mes.sin_port htons(_port);socklen_t len sizeof(server_mes);pairUdpClient*,struct sockaddr_in* thread_ptr \{this,server_mes};pthread_t rtid,wtid;pthread_create(rtid,nullptr,SendMessage,thread_ptr);pthread_create(wtid,nullptr,ReceiveMessage,thread_ptr);pthread_join(rtid,nullptr);pthread_join(wtid,nullptr);}服务端 上述代码我们已经用ip地址和端口号标识了全网的唯一 一个进程。因此我们可以用此来认证用户和给指定用户收发消息。具体采用unordered_mapstring,sockaddr_in的结构进行实现。 server.hpp增删代码
//类外
#includeunordered_map#includefunctional
using fun_t functionstring(string,string,uint16_t);//类内void CheckUser(const sockaddr_in user){string cip inet_ntoa(user.sin_addr);uint16_t port ntohs(user.sin_port);string key cip to_string(port);auto it users.find(key);if(it users.end()){cout add a new user[ cip ] endl;users.insert({key,user});}}void BroadCast(const string mes){int cnt 0;for(auto user : users){sockaddr_in client_mes user.second;socklen_t len sizeof(sockaddr_in);ssize_t n sendto(_sockfd,mes.c_str(),mes.size()\,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,send message fail,waring message\is %s,strerror(errno));continue;}cnt;}}void Run(fun_t cal_back){for(;;){char buffer[1024] {0};struct sockaddr_in client_mes; //用户的网络信息// socklen_t len sizeof(client_mes);socklen_t len;//收消息ssize_t n recvfrom(_sockfd,buffer,sizeof(buffer) \- 1,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,recvfrom message fail,waring message \is %s,strerror(errno));continue;}buffer[n] 0;string cip inet_ntoa(client_mes.sin_addr);uint16_t cport ntohs(client_mes.sin_port);string echo_mes cal_back(buffer,cip,cport);//1.检查用户是否已经上线CheckUser(client_mes);//2.广播给所有用户BroadCast(echo_mes);}server.cc——更新代码
//添加此函数。
string Print(const string mes,string ip,uint16_t port)
{string infor [ ip : to_string(port) ] \ mes;return infor;
}void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 2){Usage(argv[0]);return 1;}uint16_t port stoi(argv[1]);UdpServer* ser new UdpServer(port);ser-Init();//更新此处ser-Run(Print);return 0;
}效果 以上我们使用终端来重定向输出看起来是不太漂亮的使用图形库的知识我们可以将效果做的更为逼真更加真实。上述更新代码我们称之为版本2基于版本1更改。在运行可执行程序时我们将标准错误流重定向到指定的终端文件即可。 其次既然能收发消息我们还可以将消息当做指令进行处理就类似与我们使用ssh登录云服务器的功能类似 相关接口 //头文件#include stdio.hFILE *popen(const char *command, const char *type);
/*参数1.要执行的命令。2.打开文件的类型这里我们设置为r 模式即可。
返回值1.失败返回空指针。2.成功返回对应的文件指针。
*/int pclose(FILE *stream);/*参数要关闭的文件指针。返回值1.失败返回-1。2.成功返回0.
*/char *fgets(char *s, int size, FILE *stream);
/*参数1.存放的缓存区的地址2.缓存区的大小。3.读取的文件指针
返回值1.成功返回读取到的内容的地址。2.失败返回空指针。
*/server.cc
#includeiostream
#includevector
#includeudpserver.hpp//过滤掉一下关键词。
bool SafeCheck(const string buf)
{vectorstring key_words {rm,cp,mv,yum,top,while,};for(string word : key_words){auto it buf.find(word);if(it ! string::npos) return true;}return false;
}
//主要功能函数
string HandlerCommand(const string buf,string ip,uint16_t port)
{if(SafeCheck(buf)) return Bad Man!;FILE* res popen(buf.c_str(),r);if(res nullptr){lg(CRIT,run a command fail,error message is %s,\error is %d,strerror(errno),errno);exit(-1);}string ret;//从执行的命令的结果中读取内容while(true){char buffer[1024] {0};if(fgets(buffer,sizeof(buffer),res) nullptr)break;ret buffer;}int n pclose(res);return ret;
}
void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 2){Usage(argv[0]);return 1;}uint16_t port stoi(argv[1]);UdpServer* ser new UdpServer(port);ser-Init();ser-Run(HandlerCommand);return 0;
}server.hpp
#includefunctional
using fun_t functionstring(string,string,uint16_t);//类内void Run(fun_t cal_back){for(;;){char buffer[1024] {0};struct sockaddr_in client_mes; //用户的网络信息// socklen_t len sizeof(client_mes);socklen_t len;//收消息ssize_t n recvfrom(_sockfd,buffer,sizeof(buffer) \- 1,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,recvfrom message fail,waring message \is %s,strerror(errno));continue;}buffer[n] 0;string cip inet_ntoa(client_mes.sin_addr);uint16_t cport ntohs(client_mes.sin_port);string echo_mes cal_back(buffer,cip,cport);n sendto(_sockfd,echo_mes.c_str(),echo_mes.size()\,0,(sockaddr*)client_mes,len);if(n 0){lg(WARNNING,send message fail,waring message\is %s,strerror(errno));continue;}}}上述实现代码我们称之为版本3。基于版本1进行拓展。 总结 版本1—— 简单使用套接字编程并使用服务器和客户端完成简单的收发消息。版本2—— 基于版本1实现了一个简单的聊天室使用多线程和重定向使收消息和发信息完成并发。版本3—— 基于版本1实现了客户端远程控制服务器并执行对应发送的命令。 3.TCP
1. 基本接口
因为TCP是可靠的那么必然得多做一些准备工作具体以接口的形式呈现下面我们先介绍TCP的服务端和客户端多做的工作。
服务端
socket时我们需要设置第二个选项为SOCKET_STREAM即基于字节流的形式的协议。服务器在bind之后需要监听客户端的连接。
//头文件
#includesys/type.h
#includesys/socket.h//函数声明:
int listen(int sockfd, int backlog);
/*
参数1.SOCKET_STREAM,即TCP类型的套接字文件描述符。2.请求队列的最大长度设置为5即可,可以理解为待处理的客户端的最大连接数。
返回值1.成功返回 0.2.失败返回-1设置合适的错误码。
*/服务器在listen之后如果有客户端连接需要接收。
//头文件
#includesys/type.h
#includesys/socket.h
//函数声明
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
参数1.TCP类型的套接字文件描述符。2.输出型参数即客户端的信息。3.输入输出型参数具体的套接字地址结构体的大小变量的地址。
返回值1.成功返回对应客户端的套接字文件描述符。2.失败返回-1设置合适的错误码。
*/疑问socket 不是已经有文件描述符了accept还要返回文件描述符呢 解释 我们最开始创建的描述符是用来接收客户端连接的并不用与客户端通讯。这就好比一家餐厅有出门接客的服务员有餐桌上提供实际服务的服务员开始创建的描述符就好比出门接客的服务员而accept返回的套接字描述符就是实际提供服务的服务员。出门引客的文件描述符在引完 客人又回到店门口去引客了因此只有一个又因为店里可能同时有多人在吃饭所以实际服务的文件描述符可能有多个。在提供服务的描述符服务完之后需要关闭描述符即清理餐桌等待为下一位客人提供服务。 说明因为要保证可靠因此实际服务一次只能服务一位。 客户端只需要在一些基础工作之上与服务器建立连接即可。
//头文件
#includesys/type.h
#includesys/socket.h
//函数声明
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
参数1.TCP类型的文件描述符。2.输出型参数即客户端的信息。3.输入输出型参数具体的套接字地址结构体的大小。
返回值1.成功返回0表示连接成功。2.失败返回-1表示连接失败。
*/接口的功能基本了解之后我们可以开始实现一个简单的服务端与客户端。 实现代码文件基本框架
2. 客户端
client.cc
#includetcpclient.hpp
#includememory
void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ ip port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 3){Usage(argv[0]);return 1;}string ip argv[1];uint16_t port stoi(argv[2]);std::unique_ptrTcpClient tc(new TcpClient(ip,port));// tc-Init();tc-Run();return 0;
}tcpclient.hpp
#pragma once#include string
#include cstring
#include strings.h#include sys/types.h
#include sys/socket.h#include netinet/in.h
#include arpa/inet.h#include string
#include ../Tools/Log.hppusing std::string;enum
{CREATE_FAIL 1,TOIP_FAIL,BIND_FAIL,LISTEN_FAIL,CONNET_FAIL,IPTONET_FAIL,
};
using std::string;
uint16_t defaultport 8080;
string defaultip 59.110.171.164;
int defaultbacklog 5;
class TcpClient
{
public:TcpClient(string ip defaultip,uint16_t port defaultport): _port(port), _ip(ip), _sockfd(0){}void Run(){//server端sockaddr_in server;memset(server,0,sizeof(server));socklen_t len sizeof(server);if(inet_aton(_ip.c_str(), server.sin_addr) 0){lg(CRIT,inet_atoncreat fail,reason is %s,errno \is %d, strerror(errno), errno);exit(IPTONET_FAIL);}server.sin_family AF_INET;server.sin_port htons(_port);while(true){_sockfd socket(AF_INET, SOCK_STREAM, 0);if (_sockfd 0){lg(CRIT, socket creat fail,reason is %s,errno \is %d, strerror(errno), errno);exit(CREATE_FAIL);}bool reconect false;int cnt 10;do{int ret connect(_sockfd, (sockaddr *)server,\sizeof(server));if (ret 0){reconect true;lg(WARNNING, connect creat fail,reason \is %s,errno is %d, strerror(errno), errno);sleep(2);}else{reconect false;}} while (reconect cnt--);//发消息string str;cout please enter;getline(cin,str);ssize_t n sendto(_sockfd,str.c_str(),str.size()\,0, (sockaddr *)server, sizeof(server));if(n 0){lg(WARNNING, sendto fail,reason is %s,\errno is %d, strerror(errno), errno);continue;}char buffer[1024] {0};n recvfrom(_sockfd,buffer,sizeof(buffer) - 1,\0, (sockaddr *)server, len);if(n 0){lg(WARNNING, recvfrom fail,reason is %s,\errno is %d, strerror(errno), errno);continue;}cout buffer;close(_sockfd);}}private:int _sockfd;uint16_t _port;string _ip;
};套接字文件描述符在提供一次服务之后立马关闭重新连接。重新连接时可能会连接失败因此提供了10次重连功能重连失败跳出循环重连成功继续享受服务器的服务。
3. 服务端
server.cc:
#includetcpserver.hpp
#includememory
using std::unique_ptr;
void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 2){Usage(argv[0]);return 1;}uint16_t port stoi(argv[1]);unique_ptrTcpServer tp(new TcpServer(port));tp-Init();tp-Run();return 0;
}1.版本1
server.hpp
#pragma once
#includeunistd.h
#includestring
#includevector
#includestrings.h
#includecstring#includesys/types.h
#includesys/socket.h#includenetinet/in.h
#includearpa/inet.h#include../Tools/Log.hpp
enum
{CREATE_FAIL 1,TOIP_FAIL,BIND_FAIL,LISTEN_FAIL,
};
using std::string;
uint16_t defaultport 8080;
string defaultip 0.0.0.0;
int defaultbacklog 5;class TcpServer
{public:TcpServer(uint16_t port defaultport,string ip defaultip\,int backlog defaultbacklog):_port(port),_ip(ip),_sockfd(0),_backlog(5){lg.ReDirect(CLASSFILE);}~TcpServer(){if(_sockfd 0){close(_sockfd);}}void Init(){_sockfd socket(AF_INET,SOCK_STREAM,0);if(_sockfd 0){lg(CRIT,socket creat fail,reason is %s,errno\is %d.,strerror(errno),errno);exit(CREATE_FAIL);}//补充防止服务器偶发性无法重启即端口号无法重复的进行使用。int opt 1;setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,\opt,sizeof(opt));//初始化服务器信息sockaddr_in server_mes;socklen_t len sizeof(server_mes);memset(server_mes,0,sizeof(server_mes));server_mes.sin_port htons(_port);server_mes.sin_family AF_INET;if(!inet_aton(_ip.c_str(),server_mes.sin_addr)){lg(CRIT,address change fail,reason is %s,errno \is %d.,strerror(errno),errno);exit(TOIP_FAIL);}//绑定if(bind(_sockfd,(sockaddr*)server_mes,len) -1){lg(CRIT,bind fail,reason is %s,errno \is %d.,strerror(errno),errno);exit(BIND_FAIL);}//监听if(listen(_sockfd,_backlog) 0){lg(CRIT,listen fail,reason is %s,errno \is %d.,strerror(errno),errno);exit(LISTEN_FAIL);}lg(INFORE,sockfd is %d,TcpServer Init Success!,_sockfd);}void Run(){for(;;){sockaddr_in client_mes;memset(client_mes,0,sizeof(client_mes));socklen_t len sizeof(client_mes);int fd accept(_sockfd,(sockaddr*)client_mes,len);if(fd 0){lg(WARNNING,accept fail,reason is %s,\errno is %d,fd is %d,strerror(errno),errno,fd);break;}lg(INFORE,add a client, fd is %d.,fd);// version 1 —— 单进程版即一次只能服务一个人。Service(client_mes,len,fd); }}void Service(const sockaddr_in client,socklen_t len,int fd){//收消息while(true){char buffer[1024] {0};ssize_t n read(fd,buffer,sizeof(buffer) - 1);if(n 0){lg(WARNNING,read fail,reason is %s,\errno is %d.,strerror(errno),errno);continue;}//细节1读不到要跳出/返回。else if(n 0){break;}buffer[n] \0;string ip inet_ntoa(client.sin_addr);uint16_t port ntohs(client.sin_port);string echo_mes [ ip : to_string(port)\ ] string(buffer);n write(fd,echo_mes.c_str(),echo_mes.size());if(n 0){lg(WARNNING,sendto fail,reason is %s,\errno is %d.,strerror(errno),errno);continue;}}}
private:int _sockfd;uint16_t _port;string _ip;int _backlog;
};效果 这里我们介绍一个小工具——telnet用与简单的收发消息。使用 telnet 【IP地址】 【端口号】连接成功即可接收和发送信息。输入 ctrl 加 ]跳转到命令端口。输入回车退出命令窗口输入quit退出telnet工具。 效果 但是这个服务器有一个缺陷就是一次只能服务一位客户因为Service调用完之后才能给下一个用户提供服务那么我们可以将Run函数的Service变成如下情况实现多进程版的并发服务。
2.版本2
更改Run函数的代码
void Run(){for(;;){sockaddr_in client_mes;memset(client_mes,0,sizeof(client_mes));socklen_t len sizeof(client_mes);int fd accept(_sockfd,(sockaddr*)client_mes,len);if(fd 0){lg(WARNNING,accept fail,reason is %s,\errno is %d,fd is %d,strerror(errno),errno,fd);break;}lg(INFORE,add a client, fd is %d.,fd);// version 2 —— 使用孙子进程进行托孤即让操作系统接管。pid_t pid fork();if(pid 0){//子进程if(fork() 0) exit(0);//孙子进程Service(client_mes,len,fd); exit(0);}}}这里如果我们使用子进程的话下面势必还有父进程等待回收子进程。因此我们创建子进程之后再创建一个孙子进程并退出子进程。这样孙子进程就变为了孤儿进程由系统进行管理而父进程不受影响。 但是多进程势必会占用更多的资源因此我们还可以创建线程从而达到资源方面的优化作用。 3.版本3
在服务端增加与更改代码
//类外//声明
class TcpServer;struct PthreadData
{PthreadData(TcpServer* const ptr,socklen_t leng,\sockaddr_in client_mes,int sockfd):tp(ptr),len(leng),client(client_mes),fd(sockfd){}TcpServer* tp;socklen_t len;sockaddr_in client;int fd;
};//类内static void * Routine(void *args){//为了省略之后的join,因此直接分离线程即可。pthread_detach(pthread_self());auto p static_castPthreadData*(args);sockaddr_in client p-client;socklen_t len p-len;TcpServer* tp p-tp;int fd p-fd;tp-Service(client,len,fd);close(fd);return nullptr;}void Run(){for(;;){sockaddr_in client_mes;memset(client_mes,0,sizeof(client_mes));socklen_t len sizeof(client_mes);int fd accept(_sockfd,(sockaddr*)client_mes,len);if(fd 0){lg(WARNNING,accept fail,reason is %s,\errno is %d,fd is %d,strerror(errno),errno,fd);break;}lg(INFORE,add a client, fd is %d.,fd);// version 3 —— 多线程版pthread_t tid;PthreadData PD(this,len,client_mes,fd);pthread_create(tid,nullptr,Routine,PD);}}除此之外线程的开辟和销毁也是有一定的损耗的在此基础上我们还可以进一步的进行优化。可以利用池化技术即一次申请够线程然后一直用着这就所谓的线程池。
4.版本4
线程池threadpool.hpp
#pragma once
#includevector
#includequeue
#include Task.hpp
using std::vector;
using std::queue;
using std::cout;
using std::endl;typedef void(*cal)();
class ThreadPool
{
public:static ThreadPool* GetInstance(){if(tpool nullptr){tpool new ThreadPool();}return tpool;}void Lock(){pthread_mutex_lock(_t_mutex);}void UnLock(){pthread_mutex_unlock(_t_mutex);}~ThreadPool(){for(int i 0; i _capacity; i){pthread_join(tids[i],nullptr);}}void start(){for(int i 0; i _capacity; i){pthread_create(tids[i],nullptr,handler,this);}}void Push(const Task data){//push这里只有主线程在push,因此没必要加锁。_que.push(data);pthread_cond_broadcast(_t_cond);}static void* handler(void* args){ThreadPool* ptr static_castThreadPool*(args);ptr-_handler();}void _handler(){while(true){Lock();while(_que.empty()){pthread_cond_wait(_t_cond,_t_mutex);}_que.front()();_que.pop();UnLock();}}
private:static ThreadPool* tpool;ThreadPool(int num defaultnum):_capacity(num),tids(num){pthread_mutex_init(_t_mutex,nullptr);pthread_cond_init(_t_cond,nullptr);}const static int defaultnum 5; //线程的锁和条件变量pthread_cond_t _t_cond;pthread_mutex_t _t_mutex;queueTask _que; //任务的场所vectorpthread_t tids;int _capacity;int cnt 0;
};
ThreadPool* ThreadPool::tpool nullptr;除此之外我们还需封装对应的Task任务供线程池调用以及服务端进行推送对应的任务。而且我们还可以基于此写一个网络版本的简单的翻译词典 生成一个dict.txt放入以 : 作为分割符的中译英的单词。写一个类用于读取文件内容生成词典具体可用unordered_map。根据此类写一个Task任务。 Dict.cc
#includeiostream
#includefstream
#includeunordered_map
using namespace std;
struct Dict
{Dict(string dir /home/shun_hua\/linux_-code/test_2024/2/Dict/dict.txt){const char* _dir dir.c_str();string str;std::ifstream fs(_dir,ios_base::in);while(getline(fs, str)){int pos str.find(:);string prev str.substr(0, pos);string suf str.substr(pos 1);dict[prev] suf;}}string translate(const string word){if(dict[word] ) return unknow;return dict[word];}unordered_mapstring, string dict;
};单词可以用ChatGpt生产这里就不再列出了。 Task.hpp
#pragma once
#includeunistd.h
#includestring
#includevector
#includeunordered_map#includestrings.h
#includecstring#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h#includeLog.hpp
#include../Dict/dict.hpp
struct Task
{Task(const sockaddr_in client,socklen_t len,int fd):_client(client),_len(len),_fd(fd){}void Service(){//收消息while(true){char buffer[1024] {0};ssize_t n read(_fd,buffer,sizeof(buffer) - 1);if(n 0){lg(WARNNING,read fail,reason is %s,errno is %d,\fd is %d.,strerror(errno),errno,_fd);continue;}//细节1读不到要跳出/返回。else if(n 0){break;}buffer[n] \0;cout buffer endl;Dict dic;string ip inet_ntoa(_client.sin_addr);uint16_t port ntohs(_client.sin_port);string echo_mes [ ip : to_string(port)\ ] dic.translate(buffer) \n;cout echo_mes;n write(_fd,echo_mes.c_str(),echo_mes.size());if(n 0){lg(WARNNING,sendto fail,reason is %s,errno \is %d.,strerror(errno),errno);continue;}}}void operator()(){Service();close(_fd);};sockaddr_in _client;socklen_t _len;string user;int _fd;
};Server.hpp
#pragma once
#includeunistd.h
#includestring
#includevector
#includestrings.h
#includecstring#includesys/types.h
#includesys/socket.h#includenetinet/in.h
#includearpa/inet.h#include../Tools/Log.hpp
#include../Tools/threadpool.hpp
#include../Tools/Task.hpp
#include../Tools/daemon.hppenum
{CREATE_FAIL 1,TOIP_FAIL,BIND_FAIL,LISTEN_FAIL,
};
using std::string;
uint16_t defaultport 8080;
string defaultip 0.0.0.0;
int defaultbacklog 5;//声明
class TcpServer;struct PthreadData
{PthreadData(TcpServer* const ptr,socklen_t leng\,sockaddr_in client_mes,int sockfd):tp(ptr),len(leng),client(client_mes),fd(sockfd){}TcpServer* tp;socklen_t len;sockaddr_in client;int fd;
};ThreadPool* thp ThreadPool::GetInstance();
class TcpServer
{public:TcpServer(uint16_t port defaultport,\string ip defaultip,int backlog defaultbacklog):_port(port),_ip(ip),_sockfd(0),_backlog(5){// lg.ReDirect(CLASSFILE);}~TcpServer(){if(_sockfd 0){close(_sockfd);}}void Init(){_sockfd socket(AF_INET,SOCK_STREAM,0);if(_sockfd 0){lg(CRIT,socket creat fail,reason is %s\,errno is %d.,strerror(errno),errno);exit(CREATE_FAIL);}//补充防止服务器偶发性无法重启即端口号无法重复的进行使用。int opt 1;setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT\,opt,sizeof(opt));//初始化服务器信息sockaddr_in server_mes;socklen_t len sizeof(server_mes);memset(server_mes,0,sizeof(server_mes));server_mes.sin_port htons(_port);server_mes.sin_family AF_INET;if(!inet_aton(_ip.c_str(),server_mes.sin_addr)){lg(CRIT,address change fail,reason is %s\,errno is %d.,strerror(errno),errno);exit(TOIP_FAIL);}//绑定if(bind(_sockfd,(sockaddr*)server_mes,len) -1){lg(CRIT,bind fail,reason is %s,\errno is %d.,strerror(errno),errno);exit(BIND_FAIL);}//监听if(listen(_sockfd,_backlog) 0){lg(CRIT,listen fail,reason is %s,\errno is %d.,strerror(errno),errno);exit(LISTEN_FAIL);}lg(INFORE,sockfd is %d,TcpServer Init Success!,_sockfd);}void Run(){thp-start();for(;;){sockaddr_in client_mes;memset(client_mes,0,sizeof(client_mes));socklen_t len sizeof(client_mes);int fd accept(_sockfd,(sockaddr*)client_mes,len);if(fd 0){lg(WARNNING,accept fail,reason is %s,errno is \%d,fd is %d,strerror(errno),errno,fd);break;}lg(INFORE,add a client, fd is %d.,fd);pthread_create(tid,nullptr,Routine,PD);//version 4 —— 线程池版本thp-Push(Task(client_mes,len,fd));}}
private:int _sockfd;uint16_t _port;string _ip;int _backlog;
};三、守护进程 在之前的信号这篇文章我们对前台和后台有了基本的认识两者的区分在于是否接收键盘的信息前台接收后台不接收。相关命令 jobs 查看后台进程可执行程序 将程序变为后台。fg 任务号将程序变为前台。Ctrl Z将程序停止。bg 任务号将程序变为后台。 效果使sleep 200 为可执行程序这里是为了演示具体无意义。 会话 那么我们所谓前台和后台运行的交替从而达成信息的交互的过程我们称之为会话。 一般我们在登录服务器成功时会有一个会话这个会话在前台显示的是bash进程用于与用户实现交互后台是一些系统的进程在运行其中bash进程随着用户的登录而出现随着用户的退出而消失。 因此当登录时会话的id与bash进程的ID保持一致而且我们称这个进程为会话的领导者也就是没人变为前台那么bash就变为前台。 守护进程 一个进程自成一个会话且无终端即不与用户交流但可以将信息导入到文件中。要想变为守护进程自身不能是会话的领导者。 实现代码
#pragma once
#includeunistd.h
#includestring
#includesys/types.h
#includesys/stat.h
#includefcntl.h
#includesignal.h
using std::string;
string ddir /dev/null;
void Daemon(string dir )
{//对一些信号进行忽略确保守护进程能够不受影响的正常运行。signal(SIGPIPE,SIG_IGN);signal(SIGCHLD,SIG_IGN);//创建进程,父进程退出使子进程变为孤儿进程。if(fork() 0) exit(0);//子进程setsid();//更改当前的工作目录if(dir ! ){chdir(dir.c_str());}//将输出输入标准错误设置到 /dev/nullint fd open(ddir.c_str(),O_WRONLY);if(fd 0) return;dup2(fd,1);dup2(fd,2);dup2(fd,0);
}系统调用接口 //头文件
#include unistd.h
//函数
int daemon(int nochdir, int noclose);
/*
参数1.如果为0,则改变当前进程的工作目录为根目录否则不做变化。2.如果为0,则改变stdin,stdout,stderror为/dev/null,否则不做变化。说明这里的/dev/null看做一个 “黑洞”即输入东西没有反应。
返回值1.成功返回0。2.失败返回-1并且设置合适的错误码。
*/我们可以将我们写的进程守护进程化即7 * 24小时不间断的运行并把日志信息重定向到文件中方便出错时进行查看。 尾序 本篇文章主要是对于Socket套接字的实战 对于UDP实现了服务端两个系统的客户端实现了一个简单版本的网络聊天室和输入命令远端控制。 对于TCP实现了客户端基于资源利用率和要求实现了四个版本的服务端并在此基础上写了一个简单版本的网络之间的单词翻译。 再此基础上介绍了守护进程可以将我们写的服务端守护进程化即7*24小时不停的运作。 希望本篇文章对各位C友有所帮助下篇文章将进入自定义协议章节。 我是舜华期待与你的下一次相遇