【Linux进阶之路】Socket —— “UDP“ “TCP“

文章目录

  • 一、再识网络
    • 1. 端口号
    • 2. 网络字节序列
    • 3.TCP 与 UDP
  • 二、套接字
    • 1.sockaddr结构
    • 2.UDP
      • 1.server端
        • 1.1 构造函数
        • 1.2 Init
        • 1.3 Run
      • 2.客户端
        • 1.Linux
        • 2.Windows
    • 3.TCP
      • 1. 基本接口
      • 2. 客户端
      • 3. 服务端
        • 1.版本1
        • 2.版本2
        • 3.版本3
        • 4.版本4
  • 三、守护进程
  • 尾序

  • 温馨提示:文章较长(代码较多),请收藏起来慢慢观看。

一、再识网络

1. 端口号

 在上文我们大致理解了,网络传输的基本流程和相关概念,知道ip地址可以标识唯一的一台主机,但是主机获取到信息的最终目的是为了呈现给上层的用户,即我们所熟知的抖音等APP,既然有很多的APP,具体给哪一个APP呢?

  • 说明:
  1. APP,具体指的是运行起来的程序,即一个一个的进程。
  2. 网络通信的本质是进程之间借助网卡(共享资源)进行通信。
  • 概念
  • 端口号:
  1. 指的是用于标记进程或者服务的逻辑地址
  2. 范围为0 到 65535,分有大致三类:
  1. 系统端口:系统端口范围是从 0 到 1023,这些端口通常被一些众所周知的网络服务占用,比如 HTTP(端口 80)、HTTPS(端口 443)、FTP(端口 21)、SSH(端口 22)等。通常需要root权限才能够进行使用。
  2. 注册端口:注册端口范围是从 1024 到 49151,这一范围的端口通常被一些应用程序或服务占用。普通用户也可进行使用
  3. 动态/私有端口:动态/私有端口范围是从 49152 到 65535,也被称为私有端口或暂时端口。这些端口通常被客户端应用程序使用,用于临时通信。

疑问:既然进程的pid能标识唯一的进程,那为什么不直接捡现成的用呢?

答: 进程pid VS 端口号:

  • 从概念上看:
  1. pid:操作系统管理进程使用。
  2. 端口号:网络通信以及为应用程序提供服务。
  3. 两者实现的解耦合的关系。
  • 从使用形式上看:
  1. pid: 进程创建时才拥有。
  2. 端口号:固定一段范围,0 到 65535。
  • 从用法来看:
  1. pid: 一个进程只能有一个pid。
  2. 端口号:一个进程能有多个端口号,为用户提供不同的服务。
  3. 联系:pid和端口号都是只能对应一个进程。且通过端口号可找到进程pid,从而找到进程。

  • 总结:
  1. 通过IP地址标识唯一的一台主机。
  2. 通过端口号标识唯一的一个进程。
  3. 进而我们可以实现网络之间的通信。

拓展:在实际进行通信的过程中,一般是由客户端访问服务器,由服务器再提供对应的服务。

  • 说明:
  1. 客户端要想访问服务器,首先得知道服务器的ip地址和对应服务的端口号。这些工作早已经由开发人员做好,因此无需担心。
  2. 服务器的ip地址和端口号一般是不能发生变化的,否则客户端就无法访问。因为客户端的载入的服务器的端口号和ip一般是固定的。
  3. 客户端的端口号是动态变化的。这是因为多个app的开发厂商并不互通,因此可能存在端口号冲突的现象,因此要动态绑定端口号,而且这样做更加灵活,安全,高效。
  4. 服务器要对大量用户提供服务,而且用户的IP地址是随机变化的,这也间接的导致了,服务器要在"客户端做一些手脚", 即固定服务器的ip地址和端口号。

2. 网络字节序列

 关于数据用大端还是用小端,就跟鸡蛋先吃大头还是先吃小头一样,没有实际的争论意义,因此我们看到电脑既有大端机,也有小端机。

在这里插入图片描述

  • 说明:big - endian 为大端机的数据,little - endian为小端机的数据。
  • 速记:大同小异反着记——大 “异” 小 “同”。
  • 但是网络里面传输数据,不可能即传输大端数据也传输小端,因此规定统一在网络里面传输大端数据,到对应的主机里面再进行统一的转换,大端不用变,小端再转换一下即可。

  • 相关的接口:

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
  • 速记:h(host) to n(net) l(long),即将long类型的数据从主机转网络。其余类似。

3.TCP 与 UDP

传输方式:

  • TCP:面向字节流。
  • UDP:面向数据报。

这是最本质的差别,下面我们就这两点进行分析:

  • 面向字节流
  1. 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送,接收方按照接收到的顺序重新组装数据。
  2. 提供了可靠的传输,保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。
  3. 基于连接的通信方式,需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销,但可以确保数据的有序传输。
  4. 适用于需要可靠传输和有序性的应用,如文件传输、视频流传输等。

总结:TCP协议,面向字节流,因此可靠,有连接。适合文件和视频等信息的传输。

  • 面向数据报
  1. 将数据划分为独立的数据,即数据报,每个数据报都携带了完整的信息,可以独立地发送和接收。
  2. 不保证数据的可靠性,每个数据报都是独立传输的,可能会发生丢失、重复或乱序。
  3. 无连接的通信方式,每个数据报都是独立的,不需要事先建立连接。
  4. 对实时性要求较高的应用,如实时音频、视频通信等,因为它可以提供更低的延迟。

总结:UDP协议,面向数据报,因此不可靠,无连接。适用于对实时性要求高的应用。

  • 说明:这里的可靠和不可靠是一个中性词。不可靠意味着较低的成本,实现更加简单,可靠意味着实现需要较大的代价。因此没有谁好谁坏。

下面我们实现是更为简单的UDP套接字。

  • 在开始之前我们先来解决一个前置问题,主要是服务器的端口问题,一般默认有些端口是禁掉的,不能用于网络之间的通信,因此我们需要开放一些端口供我们之间通信使用。
  • 实现步骤:
  1. 登录所在云服务的官网。(我的是阿里云的)
  2. 点击控制台。
  3. 点击云服务器ESC/轻量级服务器/云服务器,找到对应的云服务器。(我的是轻量级云服务器)
  4. 如果是云服务器ESC/服务器就找到安全组,点击安全组ID进行编辑即可。如果是轻量级服务器就在服务器一栏找到实例id点击,再点击防火墙进行编辑即可。
  • 具体步骤——阿里云轻量级云服务器
    • 第一步:
      在这里插入图片描述
    • 第二步:
      在这里插入图片描述
    • 第三步:
      在这里插入图片描述
    • 第四步:
      在这里插入图片描述

二、套接字

1.sockaddr结构

  • 这是一层抽象化的结构,设计之初是为了统一网络套接字的接口使用,是一套通用的网络套接字,而对应的具体的套接字有 网络套接字 与 域间套接字

图解:
在这里插入图片描述

  • 类似多态的思想,即从抽象到具体。在使用过程中我们可以通过传入通用的套接字类型,并且指定对应的套接字大小,从而说明其对应的具体类型,也就是我们说的多态。

  • 我们实现的是网络编程,使用的是:struct sockaddr_in

  • 具体结构:
    在这里插入图片描述
  1. sin_family_t sin_family; 所属家族协议类型,一般设置为AF_INT/PF_INT,即ipv4类型的协议。
  2. in_port_t sin_port; 端口号。
  3. struct in_addr sin_addr; ip地址。
  • 注意:端口号和ip地址的数据都为网络序列。

2.UDP

  • Log.hpp(记录日志信息)
#pragma once
#include<map>
#include<iostream>
#include<cstdio>
#include<stdarg.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<time.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类型进行映射转换。
map<int,string> Mode = {{1,"EMERG"},{2,"ALERK"},{3,"CRIT"},{4,"ERRO"},{5,"WARNING"},{6,"NOTICE"},{7,"INFOR"},{8,"DEBUG"},{9,"NONE"}
};//分类文件处理的后缀。
map<int,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端

  • 基本框架:
//所用容器
#include<string>
#include<unordered_map>//与内存相关的头文件
#include<string.h>
#include<strings.h>//网络相关的头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h> //包装器
#include<functional>//日志头文件
#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 构造函数
  1. 一般我们使用1024以上的端口号即可,此处我们默认使用8080端口。
  2. 云服务器,禁止直接绑定公网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
  1. 创建套接字
  • 接口
//头文件:
#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。*/
  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 类型的,中间需要转换一下。

  • 实现代码:

#include<string>
#include<iostream>
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;
}
  1. 我们将字符串分为四部分,然后转换为char类型的四个变量,存储即可。
  2. 这四个部分我们存放在数组或者单独存都可以,这里我采用数组便于操作。
  3. 如果为数组,具体转换为int变量时,应注意四个部分的存储顺序。
  • 运行结果:

在这里插入图片描述

  • 说明:
  1. 我所在的机器为小端机,数据是低位放在低地址处,所以应该倒着存每一段。
  2. 如果为大端机,数据是高位放在低地址处,所以应该正着存每一段。
  3. 最后强转取数据即可。
  • 补充: 指针指向的是对象的低地址处。

在实际编程的过程中,相应的接口已经准备好,不需要手动的写,但相应的原理还是要清楚的。

  • 字符串转地址的网络序列接口:
//头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int 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
  1. 等待客户发信息

接口

//头文件
#include <sys/types.h>
#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);/*
参数:1.文件描述符。2.缓存区,读取用户发来的信息。3.缓存区的大小。4.一般使用默认值0即可。5.src_addr变量的地址,用于接收用户的网络信息,输出型参数。6.addrlen用于接受用户的src_addr具体对象的长度,输入输出型参数。返回值:1.成功,返回接受的字节个数。2.连接关闭,返回0。3.错误返回-1.设置合适的错误码。
*/
  1. 给客户提供服务

接口:

//头文件:
#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)	
参数: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
//所用容器
#include<string>
#include<unordered_map>//与内存相关的头文件
#include<string.h>
#include<strings.h>//网络相关的头文件
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h> //包装器
#include<functional>//日志头文件
#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
#include<iostream>
#include<vector>
#include"udpserver.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
  • 实际上只有四步:
  1. 创建网络套接字。

  2. 输入要发送的消息。

  3. 发消息。

  4. 收消息。

  • 注意:
  1. 在客户端,我们并不需要主动bind端口号,而是应该由系统自动分配端口号,这样即避免了不同应用程序之间端口号的冲突,也变向的提高了安全性,灵活性。
  2. 端口号在调用sento函数时,自动进行绑定。
  • client.hpp
#pragma once
//容器
#include<string>//内容接口
#include<string.h>
#include<strings.h>//网络相关的接口
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h> //线程
#include<pthread.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#include<iostream>
#include<string>#include<WinSock2.h>
//此头文件应该包含于Windows.h之上,可能的原因是重复包含相同的声明,
//也就是没有写#pragma once之类的。#include<Windows.h>#include<cstdlib>
#include<cstring>#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
#include"WindowsClient.hpp"
//智能指针所在头文件
#include<memory>
int main()
{//使用unique_ptr确保只有一台服务器。std::unique_ptr<UdpClient> cp(new UdpClient());cp->Init();cp->Run();return 0;
}

在这里插入图片描述

  • Linux与Windows的代码相比之下,其实就构造和析构多了一点东西,其余基本完全相同。

  • 上述实现代码我们称之为版本1。

 基本的收发消息我们是可以完成的,而且我们通过端口号和ip标识了唯一的一台主机的唯一进程,这样我们可以基于此简单的实现一个基于网络的聊天室:

  • 客户端:
  1. 收消息和发消息应该用不同的线程进行执行,因此我们需要在客户端创建两个线程,收消息和发消息。
  2. 具体的实现操作我们可分为两步:
  1. 将收消息的线程输出到错误流中。
  2. 在运行时将错误流再重定向到指定的终端文件当中。
  • 查看终端文件:ls /dev/pts
    在这里插入图片描述
  • 客户端更改代码:
    static void* SendMessage(void* args){auto threadptr = static_cast<pair<UdpClient*,\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_cast<pair<UdpClient*,\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);pair<UdpClient*,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);}
  • 服务端:
  1. 上述代码我们已经用ip地址和端口号标识了全网的唯一 一个进程。
  2. 因此我们可以用此来认证用户和给指定用户收发消息。
  3. 具体采用unordered_map<string,sockaddr_in>的结构进行实现。
  • server.hpp增删代码:
//类外:
#include<unordered_map>#include<functional>
using fun_t = function<string(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.h>FILE *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
#include<iostream>
#include<vector>
#include"udpserver.hpp"//过滤掉一下关键词。
bool SafeCheck(const string& buf)
{vector<string> 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
#include<functional>
using fun_t = function<string(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. 版本1—— 简单使用套接字编程,并使用服务器和客户端完成简单的收发消息。
  2. 版本2—— 基于版本1,实现了一个简单的聊天室,使用多线程和重定向使收消息和发信息完成并发。
  3. 版本3—— 基于版本1,实现了客户端远程控制服务器,并执行对应发送的命令。

3.TCP

1. 基本接口

因为TCP是可靠的,那么必然得多做一些准备工作,具体以接口的形式呈现,下面我们先介绍TCP的服务端和客户端多做的工作。

服务端:

  1. socket时,我们需要设置第二个选项为SOCKET_STREAM,即基于字节流的形式的协议。
  2. 服务器在bind之后需要监听客户端的连接。
//头文件
#include<sys/type.h>
#include<sys/socket.h>//函数声明:
int listen(int sockfd, int backlog);
/*
参数:1.SOCKET_STREAM,即TCP类型的套接字文件描述符。2.请求队列的最大长度,设置为5即可,可以理解为待处理的客户端的最大连接数。
返回值:1.成功返回 0.2.失败返回-1,设置合适的错误码。
*/
  1. 服务器在listen之后,如果有客户端连接需要接收。
//头文件
#include<sys/type.h>
#include<sys/socket.h>
//函数声明:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
参数:1.TCP类型的套接字文件描述符。2.输出型参数,即客户端的信息。3.输入输出型参数,具体的套接字地址结构体的大小变量的地址。
返回值:1.成功返回对应客户端的套接字文件描述符。2.失败返回-1,设置合适的错误码。
*/
  • 疑问:socket 不是已经有文件描述符了,accept还要返回文件描述符呢?
  • 解释:
  1. 我们最开始创建的描述符是用来接收客户端连接的,并不用与客户端通讯。
  2. 这就好比一家餐厅,有出门接客的服务员,有餐桌上提供实际服务的服务员,开始创建的描述符就好比出门接客的服务员,而accept返回的套接字描述符就是实际提供服务的服务员。
  3. 出门引客的文件描述符在引完" 客人",又回到店门口去引客了,因此只有一个,又因为店里可能同时有多人在吃饭,所以实际服务的文件描述符可能有多个。
  4. 在提供服务的描述符服务完之后,需要关闭描述符,即清理餐桌,等待为下一位客人提供服务。
  • 说明:因为要保证可靠,因此实际服务一次只能服务一位。

客户端:只需要在一些基础工作之上,与服务器建立连接即可。

//头文件
#include<sys/type.h>
#include<sys/socket.h>
//函数声明:
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/*
参数:1.TCP类型的文件描述符。2.输出型参数,即客户端的信息。3.输入输出型参数,具体的套接字地址结构体的大小。
返回值:1.成功返回0,表示连接成功。2.失败返回-1,表示连接失败。
*/
  • 接口的功能基本了解之后,我们可以开始实现一个简单的服务端与客户端。

  • 实现代码文件基本框架:
    在这里插入图片描述

2. 客户端

  • client.cc
#include"tcpclient.hpp"
#include<memory>
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_ptr<TcpClient> 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.hpp"using 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:
#include"tcpserver.hpp"
#include<memory>
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_ptr<TcpServer> tp(new TcpServer(port));tp->Init();tp->Run();return 0;
}
1.版本1
  • server.hpp
#pragma once
#include<unistd.h>
#include<string>
#include<vector>
#include<strings.h>
#include<cstring>#include<sys/types.h>
#include<sys/socket.h>#include<netinet/in.h>
#include<arpa/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,用与简单的收发消息。
  • 使用:
  1. telnet 【IP地址】 【端口号】
  2. 连接成功即可接收和发送信息。
  3. 输入 ctrl 加 ],跳转到命令端口。
  4. 输入回车退出命令窗口,输入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_cast<PthreadData*>(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
#include<vector>
#include<queue>
#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_cast<ThreadPool*>(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;queue<Task> _que; //任务的场所vector<pthread_t> tids;int _capacity;int cnt = 0;
};
ThreadPool* ThreadPool::tpool = nullptr;
  • 除此之外,我们还需封装对应的Task任务,供线程池调用,以及服务端进行推送对应的任务。
  • 而且我们还可以基于此写一个网络版本的简单的翻译词典:
    1. 生成一个dict.txt,放入以 : 作为分割符的中译英的单词。
    2. 写一个类,用于读取文件内容生成词典,具体可用unordered_map。
    3. 根据此类写一个Task任务。

  • Dict.cc
#include<iostream>
#include<fstream>
#include<unordered_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_map<string, string> dict;
};
  • 单词可以用ChatGpt生产,这里就不再列出了。

  • Task.hpp
#pragma once
#include<unistd.h>
#include<string>
#include<vector>
#include<unordered_map>#include<strings.h>
#include<cstring>#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>#include"Log.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
#include<unistd.h>
#include<string>
#include<vector>
#include<strings.h>
#include<cstring>#include<sys/types.h>
#include<sys/socket.h>#include<netinet/in.h>
#include<arpa/inet.h>#include"../Tools/Log.hpp"
#include"../Tools/threadpool.hpp"
#include"../Tools/Task.hpp"
#include"../Tools/daemon.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;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;
};

三、守护进程

  • 在之前的信号这篇文章,我们对前台和后台有了基本的认识,两者的区分在于是否接收键盘的信息,前台接收,后台不接收
  • 相关命令:
  1. jobs 查看后台进程
  2. 可执行程序 &,将程序变为后台。
  3. fg 任务号,将程序变为前台。
  4. Ctrl Z,将程序停止。
  5. bg 任务号,将程序变为后台。
  • 效果:使sleep 200 为可执行程序,这里是为了演示具体无意义。
    在这里插入图片描述
  • 会话

    • 那么我们所谓前台和后台运行的交替,从而达成信息的交互的过程,我们称之为会话。

    • 一般我们在登录服务器成功时会有一个会话,这个会话在前台显示的是bash进程,用于与用户实现交互,后台是一些系统的进程在运行,其中bash进程随着用户的登录而出现,随着用户的退出而消失。

    • 因此当登录时,会话的id与bash进程的ID保持一致,而且我们称这个进程为会话的领导者,也就是没人变为前台,那么bash就变为前台。
      在这里插入图片描述

  • 守护进程

    • 一个进程自成一个会话,且无终端,即不与用户交流,但可以将信息导入到文件中。
    • 要想变为守护进程自身不能是会话的领导者。
  • 实现代码:

#pragma once
#include<unistd.h>
#include<string>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<signal.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友有所帮助,下篇文章将进入自定义协议章节。


我是舜华,期待与你的下一次相遇!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/262940.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【区块链】智能交易模式下的数据安全流通模型

【区块链】智能交易模式下的数据安全流通模型 写在最前面**区块链智能交易模式概述****数据安全流通的挑战****数据安全流通模型的核心要素****实现数据安全流通的区块链技术****区块链智能交易模式下数据安全流通模型的设计原则****数据安全流通模型的应用案例分析****面临的挑…

金航标电子位于广西柳州鹿寨县天线生产基地于大年正月初九开工了

金航标电子位于广西柳州鹿寨县天线生产基地于大年正月初九开工了&#xff01;&#xff01;&#xff01;金航标kinghelm&#xff08;www.kinghelm.com.cn&#xff09;总部位于中国深圳市&#xff0c;兼顾技术、成本、管理、效率和可持续发展。东莞塘厦实验室全电波暗室、网络分析…

C++初阶:容器适配器priority_queue常用接口详解及模拟实现、仿函数介绍

介绍完了stack和queue的介绍以及模拟的相关内容后&#xff1a;C初阶&#xff1a;容器适配器介绍、stack和queue常用接口详解及模拟实现 接下来进行priority_queue的介绍以及模拟&#xff1a; 文章目录 1.priority_queue的介绍和使用1.1priority_queue的初步介绍1.2priority_que…

MySQL数据库调优之 explain的学习

性能分析工具的使用 在数据库调优中&#xff0c;目标就是响应时间更快&#xff0c;吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮助快速找到调优的思路与方式。 1.数据库服务器的优化步骤 整个流程分为观察(Show status)和行动(Action) 两个部分。字母S的部分代表观察…

电路设计(28)——交通灯控制器的multisim仿真

1.功能设定 南北、东西两道的红灯时间、绿灯时间均为24S&#xff0c;数码管显示倒计时。在绿灯的最后5S内&#xff0c;黄灯闪烁。有夜间模式&#xff1a;按下按键进入夜间模式。在夜间模式下&#xff0c;数码管显示计数最大值&#xff0c;两个方向的黄灯不停闪烁。 2.电路设计 …

力扣645. 错误的集合(排序,哈希表)

Problem: 645. 错误的集合 文章目录 题目描述思路复杂度Code 题目描述 思路 1.排序 1.对nums数组按从小到大的顺序排序; 2.遍历数组时若判断两个相邻的元素则找到重复元素&#xff1b; 3.记录一个整形变量prev一次置换当前位置元素并与其作差&#xff0c;若差等于2着说明缺失的…

DNS域名解析过程

DNS是什么 维护一个用来表示组织内部主机名和IP地址之间对应关系的数据库。用户输入域名&#xff0c;DNS自动检索该数据库&#xff0c;并将其转换为IP地址。用的是UDP传输协议 DNS域名解析过程 域名的构成 首先要知道域名的层级&#xff0c;比如www.qq.com一般是主站&#…

备战蓝桥杯————双指针技巧巧解数组2

利用双指针技巧来解决七道与数组相关的题目。 两数之和 II - 输入有序数组&#xff1a; 给定一个按升序排列的数组&#xff0c;找到两个数使它们的和等于目标值。可以使用双指针技巧&#xff0c;在数组两端设置左右指针&#xff0c;根据两数之和与目标值的大小关系移动指针。 …

Ubuntu20.04开启/禁用ipv6

文章目录 Ubuntu20.04开启/禁用ipv61.ipv62. 开启ipv6step1. 编辑sysctl.confstep2. 编辑网络接口配置文件 3. 禁用ipv6&#xff08;sysctl&#xff09;4. 禁用ipv6&#xff08;grub&#xff09;附&#xff1a;总结linux网络配置 Ubuntu20.04开启/禁用ipv6 1.ipv6 IP 是互联网…

软考-中级-系统集成2023年综合知识(三)

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 软考中级专栏回顾 专栏…

【ArcGIS】利用高程进行坡度分析:区域面/河道坡度

在ArcGIS中利用高程进行坡度分析 坡度ArcGIS实操案例1&#xff1a;流域面上坡度计算案例2&#xff1a;河道坡度计算2.1 案例数据2.2 操作步骤 参考 坡度 坡度是地表单元陡缓的程度&#xff0c;通常把坡面的垂直高度和水平距离的比值称为坡度。 坡度的表示方法有百分比法、度数…

基于springboot+vue的靓车汽车销售网站(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

Python实战: 获取 后缀名(扩展名) 或 文件名

Python实战: 获取 后缀名(扩展名) 或 文件名 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程 &#x1f448; 希望得到您的订阅和支持~ &…

DAY29--learning English

一、积累 1.sign up for 2.business trip 3.calendar 4.acne 5.band-aid 6.scar 7.prescription 8.pimple 9.saucy 10.slurp 11.germaphobe 12.shred 13.boggle 14.platser 15.lick 16.sling 17.smack 18.stereotype 19.salmon 20.cable 二、练习 1.牛津原译 calendar. /ˈk…

9、使用 ChatGPT 的 GPT 制作自己的 GPT!

使用 ChatGPT 的 GPT 制作自己的 GPT! 想用自己的 GPT 超越 GPT ChatGPT 吗?那么让我们 GPT GPT 吧! 山姆 奥特曼利用这个机会在推特上宣传 GPTs 的同时还猛烈抨击了埃隆的格罗克。 GPTs概览 他们来了! 在上周刚刚宣布之后,OpenAI 现在推出了其雄心勃勃的新 ChatGPT…

【Django开发】0到1开发美多shop项目:Celery短信和用户注册。全md文档笔记(附代码,已分享)

本系列文章md笔记&#xff08;已分享&#xff09;主要讨论django商城项目开发相关知识。本项目利用Django框架开发一套前后端不分离的商城项目&#xff08;4.0版本&#xff09;含代码和文档。功能包括前后端不分离&#xff0c;方便SEO。采用Django Jinja2模板引擎 Vue.js实现…

day41WEB 攻防-通用漏洞XMLXXE无回显DTD 实体伪协议代码审计

本章知识点&#xff1a; 1 、 XML&XXE- 原理 & 发现 & 利用 & 修复等 2 、 XML&XXE- 黑盒模式下的发现与利用 3 、 XML&XXE- 白盒模式下的审计与利用 4 、 XML&XXE- 无回显 & 伪协议 & 产生层面 配套资源&#xff08;百度网盘&#x…

Stable Diffusion 绘画入门教程(webui)-ControlNet(IP2P)

上篇文章介绍了深度Depth&#xff0c;这篇文章介绍下IP2P&#xff08;InstructP2P&#xff09;, 通俗理解就是图生图&#xff0c;给原有图加一些效果,比如下图&#xff0c;左边为原图&#xff0c;右边为增加了效果的图&#xff1a; 文章目录 一、选大模型二、写提示词三、基础参…

Flink ML 的新特性解析与应用

摘要&#xff1a;本文整理自阿里巴巴算法专家赵伟波&#xff0c;在 Flink Forward Asia 2023 AI特征工程专场的分享。本篇内容主要分为以下四部分&#xff1a; Flink ML 概况在线学习的设计与应用在线推理的设计与应用特征工程算法与应用 一、Flink ML 概况 Flink ML 是 Apache…

(六)激光线扫描-三维重建

本篇文章是《激光线扫描-三维重建》系列的最后一篇。 1. 基础理论 1.1 光平面 在之前光平面标定的文章中,已经提到过了,是指 激光发射器投射出一条线,形成的一个扇形区域平面就是光平面。 三维空间中平面的公式是: A X + B Y + C Z + D = 0 A X+B Y+C Z+D=0