【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呢?
- 说明:
- 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.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:面向数据报。
这是最本质的差别,下面我们就这两点进行分析:
- 面向字节流
- 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送,接收方按照接收到的顺序重新组装数据。
- 提供了可靠的传输,保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。
- 基于连接的通信方式,需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销,但可以确保数据的有序传输。
- 适用于需要可靠传输和有序性的应用,如文件传输、视频流传输等。
总结:
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
#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 构造函数
- 一般我们使用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 类型的,中间需要转换一下。
-
实现代码:
#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;
}
- 我们将字符串分为四部分,然后转换为char类型的四个变量,存储即可。
- 这四个部分我们存放在数组或者单独存都可以,这里我采用数组便于操作。
- 如果为数组,具体转换为int变量时,应注意四个部分的存储顺序。
- 运行结果:

- 说明:
- 我所在的机器为小端机,数据是低位放在低地址处,所以应该倒着存每一段。
- 如果为大端机,数据是高位放在低地址处,所以应该正着存每一段。
- 最后强转取数据即可。
- 补充: 指针指向的是对象的低地址处。
在实际编程的过程中,相应的接口已经准备好,不需要手动的写,但相应的原理还是要清楚的。
- 字符串转地址的网络序列接口:
//头文件
#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
- 等待客户发信息
接口
//头文件
#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.设置合适的错误码。
*/
- 给客户提供服务
接口:
//头文件:
#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
- 实际上只有四步:
创建网络套接字。
输入要发送的消息。
发消息。
收消息。
- 注意:
- 在客户端,我们并不需要
主动bind端口号,而是应该由系统自动分配端口号,这样即避免了不同应用程序之间端口号的冲突,也变向的提高了安全性,灵活性。- 端口号在调用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标识了唯一的一台主机的唯一进程,这样我们可以基于此简单的实现一个基于网络的聊天室:
- 客户端:
- 收消息和发消息应该用不同的线程进行执行,因此我们需要在客户端创建两个线程,收消息和发消息。
- 具体的实现操作我们可分为两步:
- 将收消息的线程输出到错误流中。
- 在运行时将错误流再重定向到指定的终端文件当中。
- 查看终端文件: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);}
- 服务端:
- 上述代码我们已经用ip地址和端口号标识了全网的唯一 一个进程。
- 因此我们可以用此来认证用户和给指定用户收发消息。
- 具体采用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—— 简单使用套接字编程,并使用服务器和客户端完成简单的收发消息。
- 版本2—— 基于版本1,实现了一个简单的聊天室,使用多线程和重定向使收消息和发信息完成并发。
- 版本3—— 基于版本1,实现了客户端远程控制服务器,并执行对应发送的命令。
3.TCP
1. 基本接口
因为TCP是可靠的,那么必然得多做一些准备工作,具体以接口的形式呈现,下面我们先介绍TCP的服务端和客户端多做的工作。
服务端:
- socket时,我们需要设置第二个选项为
SOCKET_STREAM,即基于字节流的形式的协议。 - 服务器在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,设置合适的错误码。
*/
- 服务器在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还要返回文件描述符呢?
- 解释:
- 我们最开始创建的描述符是用来接收客户端连接的,并不用与客户端通讯。
- 这就好比一家餐厅,有出门接客的服务员,有餐桌上提供实际服务的服务员,开始创建的描述符就好比出门接客的服务员,而accept返回的套接字描述符就是实际提供服务的服务员。
- 出门引客的文件描述符在引完" 客人",又回到店门口去引客了,因此只有一个,又因为店里可能同时有多人在吃饭,所以实际服务的文件描述符可能有多个。
- 在提供服务的描述符服务完之后,需要关闭描述符,即清理餐桌,等待为下一位客人提供服务。
- 说明:因为要保证可靠,因此实际服务一次只能服务一位。
客户端:只需要在一些基础工作之上,与服务器建立连接即可。
//头文件
#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,用与简单的收发消息。
- 使用:
- 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_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任务,供线程池调用,以及服务端进行推送对应的任务。
- 而且我们还可以基于此写一个网络版本的简单的翻译词典:
- 生成一个dict.txt,放入以
:作为分割符的中译英的单词。 - 写一个类,用于读取文件内容生成词典,具体可用unordered_map。
- 根据此类写一个Task任务。
- 生成一个dict.txt,放入以
- 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;
};
三、守护进程
- 在之前的信号这篇文章,我们对前台和后台有了基本的认识,两者的区分在于是否接收键盘的信息,
前台接收,后台不接收。- 相关命令:
- jobs 查看后台进程
- 可执行程序 &,将程序变为后台。
- fg 任务号,将程序变为前台。
- Ctrl Z,将程序停止。
- 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友有所帮助,下篇文章将进入自定义协议章节。
我是舜华,期待与你的下一次相遇!
相关文章:
【Linux进阶之路】Socket —— “UDP“ “TCP“
文章目录 一、再识网络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. 端口号 在…...
一些用 GPT 翻译的计算机科学/人工智能 PDF 讲义
3D成像.pdf3D成像技术.pdf3D点云分析.pdfAAAI 2019 笔记.pdfCMU 10.708 概率图模型讲义.pdfCMU 15-312 编程语言基础讲义.pdfCMU 15-411 编译器设计讲义.pdfCMU 15-819 同伦类型论讲义.pdfCMU 15-819O 程序分析讲义.pdfCUNY CSci335 软件设计与分析 3 讲义.pdfDixie IT4500 信息…...
重大更新:GPT-4 API 现全面向公众开放!
重大更新:GPT-4 API 现全面向公众开放! 在 AIGC(人工智能生成内容)领域内,我们一直致力于跟踪和分析如 OpenAI、百度文心一言等大型语言模型(LLM)的进展及其在实际应用中的落地情况。我们还专注…...
【Python笔记-设计模式】对象池模式
一、说明 用于管理对象的生命周期,重用已经创建的对象,从而减少资源消耗和创建对象的开销 (一) 解决问题 主要解决频繁创建和销毁对象所带来的性能开销问题。如数据库连接、线程管理、网络连接等,对象的创建和销毁成本相对较高,…...
反序列化 [NPUCTF2020]ReadlezPHP1
打开题目 直接查看源代码 打开源代码发现了个./time.php?source 访问一下 审计代码: 现存在反序列化语句:$ppp unserialize($_GET["data"]);和执行漏洞:echo $b($a); 发现在__destruct()方法里面有 echo $b($a); 这个是php的…...
AI技术那些事儿:揭开潜伏在你生活中的高科技小能手
你有没有发现,现在的生活里有些“看不见”的聪明家伙,它们时时刻刻在帮咱们忙活呢?从早上用语音命令打开窗帘、播报新闻,到晚上喊一声关灯睡觉,这些都离不开人工智能(简称AI)的助攻。今天咱就掰…...
使用向量数据库pinecone构建应用06:日志系统异常检测 Anomaly Detection
Building Applications with Vector Databases 下面是这门课的学习笔记:https://www.deeplearning.ai/short-courses/building-applications-vector-databases/ Learn to create six exciting applications of vector databases and implement them using Pinecon…...
抽象工厂模式 Abstract Factory
1.模式定义: 提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类 2. 应用场景: 程序需要处理不同系列的相关产品,但是您不希望它依赖于这些产品的 具体类时, 可以使用抽象工厂 3.优点: 1.可以确信你从工厂得到的产品彼…...
掌握 Android 中的 RecyclerView 优化
掌握 Android 中的 RecyclerView 优化 一、RecyclerView Pool以及何时使用它二、onCreateViewHolder 和 onBindViewHolder三、优化 RecyclerView 的不同方法四、视图无效与请求布局五、ViewHolder模式六、默认的废料和脏视图类型七、结论 RecyclerView 是 Android 中一个功能强…...
Android platform tool中d8.bat不生效
d8.bat因找不到java_exe文件,触发EOF d8.bat中之前代码为: set java_exe if exist "%~dp0..\tools\lib\find_java.bat" call "%~dp0..\tools\lib\find_java.bat" if exist "%~dp0..\..\tools\lib\find_java.bat" …...
WSL安装Ubuntu22.04,以及深度学习环境的搭建
安装WSL 安装 WSL 2 之前,必须启用“虚拟机平台”可选功能。 计算机需要虚拟化功能才能使用此功能。 以管理员身份打开 PowerShell 并运行: dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart下载 Linux 内核更…...
【PTA|选择题|期末复习】结构体
2-1 For the following declarations,assignment expression_is not correct. struct Student {long num;char name[28];}st1,st2{101,"Tom"},*p&st1; 〇 A.st1 st2 ◎ B.p->name st2.name O C p->num st2.num 〇 D,*pst2 2-2 下面定义结构变量的语…...
Video generation models as world simulators-视频生成模型作为世界模拟器
原文地址:Video generation models as world simulators 我们探索在视频数据上进行大规模生成模型的训练。具体来说,我们联合训练文本条件扩散模型,同时处理不同持续时间、分辨率和长宽比的视频和图像。我们利用一个在视频和图像潜在编码的时…...
高刷电竞显示器 - HKC VG253KM
今天给大家分享一款高刷电竞显示器 - HKC VG253KM。 高刷电竞显示器 - HKC VG253KM源于雄鹰展翅翱翔的设计灵感,严格遵循黄金分割比例的蓝色点晴线条,加上雾面工艺及高低起伏错落有致的线条处理,在VG253KM的背部勾勒出宛若大鹏展翅的鹰翼图腾…...
nginx-------- 高性能的 Web服务端 (三) 验证模块 页面配置
一、http设置 1.1 验证模块 需要输入用户名和密码 htpasswd 此命令来自于 httpd-tools 包,如果没有安装 安装一下即可 也可以安装httpd 直接yum install httpd -y 也一样 第一次生成文件htpasswd -c 文件路径 姓名 交互式生成密码 htpasswd -bc 文…...
Kafka是如何防止消息丢失的
Kafka通过一系列机制来防止消息丢失,主要包括以下几个方面: 生产者端(Producer)保证: 同步发送:生产者默认是异步发送消息的,但如果希望保证消息不丢失,可以选择将异步发送改为同步…...
[工具探索]VSCode介绍和进阶使用
相比较GoLand、PhpStorm、PyCharm、WebStorm的重量级内存占用,从Windows系统来,各种卡死,换到MacOS倒不会卡死,但是内存占用太多,影响体验,决定换到VSCode。当然这个过程需要适应过渡期,旧伙计都…...
Oracle迁移到mysql-表结构的坑
1.mysql中id自增字段必须是整数类型 id BIGINT AUTO_INCREMENT not null, 2.VARCHAR2改为VARCHAR 3.NUMBER(16)改为decimal(16,0) 4.date改为datetime 5.mysql范围分区必须int格式,不能list类型 ERROR 1697 (HY000): VALUES value for partition …...
【SpringCloudAlibaba系列--nacos配置中心】
Nacos做注册中心以及使用docker部署nacos集群的博客在这: 容器化部署Nacos:从环境准备到启动 容器化nacos部署并实现服务发现(gradle) 使用docker部署nacos分布式集群 下面介绍如何使用nacos做配置中心 首先要进行nacos-config的引入,引入…...
使用LinkedList实现堆栈及Set集合特点、遍历方式、常见实现类
目录 一、使用LinkedList实现堆栈 堆栈 LinkedList实现堆栈 二、集合框架 三、Set集合 1.特点 2.遍历方式 3.常见实现类 HashSet LinkedHashSet TreeSet 一、使用LinkedList实现堆栈 堆栈 堆栈(stack)是一种常见的数据结构,一端…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
九天毕昇深度学习平台 | 如何安装库?
pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子: 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...
AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...
Linux安全加固:从攻防视角构建系统免疫
Linux安全加固:从攻防视角构建系统免疫 构建坚不可摧的数字堡垒 引言:攻防对抗的新纪元 在日益复杂的网络威胁环境中,Linux系统安全已从被动防御转向主动免疫。2023年全球网络安全报告显示,高级持续性威胁(APT)攻击同比增长65%,平均入侵停留时间缩短至48小时。本章将从…...
前端调试HTTP状态码
1xx(信息类状态码) 这类状态码表示临时响应,需要客户端继续处理请求。 100 Continue 服务器已收到请求的初始部分,客户端应继续发送剩余部分。 2xx(成功类状态码) 表示请求已成功被服务器接收、理解并处…...
CppCon 2015 学习:Reactive Stream Processing in Industrial IoT using DDS and Rx
“Reactive Stream Processing in Industrial IoT using DDS and Rx” 是指在工业物联网(IIoT)场景中,结合 DDS(Data Distribution Service) 和 Rx(Reactive Extensions) 技术,实现 …...
Ubuntu 安装 Mysql 数据库
首先更新apt-get工具,执行命令如下: apt-get upgrade安装Mysql,执行如下命令: apt-get install mysql-server 开启Mysql 服务,执行命令如下: service mysql start并确认是否成功开启mysql,执行命令如下&am…...









