文章目录
这篇博客一直写写停停拖了半个月才出来
套接字预备知识
我们所写的程序都是在用户层进行开发,或者说是在应用层进行协议的创建和规定,所以我们所用到的接口都是传输层所用到的接口,也就是系统调用接口,也就是说,我们后面所学的套接字接口,所用的都是传输层的接口
两个主机之间的通信,是不是可以认为是两个主机硬件之间的通信,
答案既可以是,也可以不是,实际上,硬件是承当两台主机进行通信的载体不错,但是,进行通信的是两台主机里面分别的进程,即两个进程之间进行通信,也即是软件层面上的
套接字,端口号,ip
IP地址(公网ip),唯一标识互联网中的一台主机
源IP,目的IP:对一个报文来说就是,从哪来到哪去,
这个最大的意义:就是指导一个报文进行路径选择,到哪里去:本质就是让我们根据目标进行路径进行路径选择的依据!
下一跳设备(mac地址的变化)
我们今天,这里更加专业和准确的来说,套接字本质就是进程之间的通信(是指不同主机之间的进程)
而ip地址的主机的唯一性,但是在这个主机上有很多进程,那么我应该怎么去确定我应该给哪一台主机呢?
所以,我们还需要通过某种方式来去表示特定主机上的某种进程,而标识进程的方式,我们就叫做端口号
端口号我们就是可以去标识一台主机里面唯一的进程
而用ip和端口号我们就能标识全网范围内的唯一主机上面的唯一进程
套接字=ip地址+端口号(端口号是一个16位的比特位)
我们学的网络通信,是站在人与人之间的通信
技术人员的视角:我们学到的网络通信,本质上是进程间的通信!!
(跨网络进行进程间通信)
比如:抖音的app客户端(进程)《-》抖音的服务器(也是一个进程)
IP仅仅知识解决了两台物理机器之间的通信
但是我们要考虑双方用户之间能够看到发送和接收数据,应用层上都有进程
端口号和PID
每个进程都会有PID,但是一个进程必须是要有网络进程才会有端口后
{注意:一个端口号只能由一个进程占用,但是一个进程可以有多个端口号}
(10086就相当于一个IP地址,(打过去之后叫了人工服务)而某一个人工就相当于是一个端口号,然后他的身份证就相当于是PID)
为了解耦(如果操作系统对于进程的标识方法变化的话,所有东西都要变化,但是如果用端口号的话,它只在网络中使用,不影响其他地方)
总结:
- 端口号就是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前这个数据是要交给哪一个进程来处理
- 一个端口用只能被一个进程占用
- ip地址+端口号能够用来标识网络上某一台主机的某一个进程
互联网世界,是一个进程间通信的世界
万物互联就是每一个机器上都有一个进程,每一个进程就可以收集各种进程间的消息,还可以和其他进程之间通信,这样就可以获得了
TCP和UDP 协议的初识
因为我们要在应用层上写程序,而离应用层最近的就是传输层,所以我们使用的就是传输层接口
TCP协议特点(初始)
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP 协议特点(初始)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
tcp的可靠和不可靠是一个中性词,在直播的时候,就是使用UDP,
标准:非用tcp不可的就是用tco,处于成本考虑,也可以使用UDP,实在不会的话就使用TCP,
TCP协议需要解决数据传错了怎么办,丢包了怎么办,对方来不及接收怎么办,网络堵塞了怎么办
就是说,他会把大量的问题都要很好的解决,这样也就直接决定了TCP 的特点(相对于UDP来说,会比较复杂)。放过来UDP就比较的简单高效
为什么还需要不可靠的传输协议
可靠意味着需要举行的更多的工作来保证可靠性,成本也会更加多,效率也会更低一点,不可靠协议的特点就是简单,高效,实际上,我们需要根据需求来选择不同的协议
网络字节序
小端法(pc本机) 大端法(网络)
我们现在的机器都是小端,但是有没有一种可能,我们这边是小端机器,但是对面的服务器是大端机呢,这样的化,我们按照小端字节序发出去的数据,到了对方的服务器上面使用大端的方式来接收,就会造成错乱
那么如何解决这个问题呢?
我们网络中规定:所有的网络中所跑到 数据都是打断的数据,如果是小端(数据的低位保存在内存的低地址中,高位保存在内存的高地址)机器,都是需要先转化成为大端(数据的地位保存在内存的高地址,高位保存在内存的低地址)之后,才能进行发送或者接收,然后先发送或者接收低地址的数据,再是高地址的数据
总结来说,其
- 发送主机通常把缓冲区的数据按照内存地址的从低到搞的顺序进行发出
- 接收主机把从网络上接到的字节一次保存再接收缓冲区里面,也是按照内存地址从低到高的顺序保存
- 因此,网络数据流的地址应该这样进行规定,先发出的数据是低地址,后发出的数据就是高地址
- TCP/IP协议规定,,网络数据流应该采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据,如果当前发送的主机是小端机,就需要先将数据转化成为大端,否者就忽略们直接发就可以了
为什么要规定成大端呢
因为这样,我们先就可以接收到低地址的数据,就是高位权重的数据
这样我们可以将高位放在高的位置,可以实现变实现边计算
同样,当我们接收字符串等待消息的时候,接收的信息就可以按照我们视觉中的从左万右出现,而不是先接收到最后一位,然后不断的头插操作
即:大端更加符合我们的阅读习惯
为了使得网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能够正常的运行,可以调用以下库函数左网络字节序和主机字节序的转换
h标识host(主机)
n标识network(网络)
例如
htonl表示将32位长整数从主机转化位网络字节序,例如将ip地址转化后准备发送(因为ip就是32位,4字节),ip协议
(192.168.0.102),这个是string我们想要使用的话,就要先把这个转化成为整数
如果主机是小端字节序。这些函数将参数左对应的大小端转换然后返回
如果是主机是大端字节序,就不用改变
htons将16位长整型数从主机转化成网络字节序,例如端口(port)
nthos,将16位长整数从网络转化为主机里面
string转到网络字节序如192.168.0.122
string->atoi->int->htol—>网络字节序
socket编程
socket创建套接字的流程
socket的API
#include<sys/type.h>
#include<sys/socket.h>int socket(int domain,int type,int protocol);//创建一个端点进行通信
返回的是socket的文件描述符,(TCP/UDP,客户端和服务器)
int bind(int sockfd ,const struct sockaddr* address,socklen_t address_len);
绑定端口号(TCP/UDP,服务器)
int listen(int socket ,int backlog)
;开始监听socket(tcp,服务器)
backlog:全连接队列的最大长度,如果有多个客户端发送过来请求,此时 没有被连接上的就会被放在一个链接队列李米娜,该传输表示的就是全连接队列的最大长度,一般不要设置太大,设置5./10即可
我们一开始创建的套接字并不是一个普通的套接字,而应该要叫做监听套接字,
int accept(int socket,struct sockaddr* address,socklen_t * address_len)’
接受请求
int connect (int sockfd,const struct sockaddr* addr,socklen_t addrlen)
建立连接
调用accept函数获取连接时,是从监听套接字里面获取的,如果accept连接成功,就会返回接收到的套接字对应的文件描述符
监听套接字和accept函数返回的套接字的作用:
- 监听套接字:用于获取客户端发来的请求,accpt函数会不断从监听套接字里面获取新的连接
- accept函数防护的套接字,用于为本次accpet获取到的连接提供服务。监听套接字的任务知识不断的获取连接,真正为这些连接提供服务的是accpet函数套接字
sockaddr 的结构
网络通信的标准又很多种,基于ip的网络通信,AF_INET,原始套接字,域间套接字
我们想让这些接口系统的统一化,sockaddr也就是一个通用结构
struct sockaddr_in把里面的14个字节进行细分了一下,然后sockaddr就被废弃了,所以我们在实际定义的时候就是定义struct sockaddr_in,在调用这个函数的时候要进行强制类型转换
#include<netinet/in.h>
- sockaddr_in(inet) 是用来进行网络通信的,
- sockaddr_un 是用来进行本地通信的 、
- sockaddr_in结构体存储了协议类型,端口号,ip地址等信息,网络通信时可以通过这个结构体把自己的信息发送给对方,也可以通过这个结构体获取远端的信息
- 可以看出,着3个结构体的前面16位都是一样的,代表的就是协议家族,可以根据这个参数判断需要进行哪一种特性(本地和跨网络)
- ipv4, ipv6的地址格式定义在netinet /in.h中,ipv4地址用socketaddr_in,结构体表示,包括16位地址类型,16位端口号,32位ip地址
- ipv4和ipv6地址类型分别定义为常数AF_INET,AF_INET6,只要取得某种socketaddr结构体的首地址,不需要知道具体是哪一种类型的结构体,就可以根据地址类型字段确定结构体中的内容
- socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化成sockaddr,这样好处是程序的通用性,可以接收ipv4和ipv6以及unix domain socket各种类型的sockaddr结构体指针为参数,
sockaddr_in的结构体:因为我们主要用到网络通信,所以这里主要介绍这个结构体,
sin_family 代表的是地址类型,我们主要用的是AF_INET,
sin_port代表的是端口号,这个是网络字节序的端口号,用之前要先htons初始化
sin_addr代表的是网络地址(也就是ip地址),用一个结构体struct in_addr来进行描述(这里填充的是ipv4的地址,一个32位整数),还要使用htonl将string转化成为网络字节序,要用inet_pton
但是这里的ip地址的类型是struct in_addr{uint32_t s_addr}
我们使用的时候就要用这个东西进行初始化
struct sockaddr_in addr;
addr.sin_family=AF_INET;
add.sin_port=htos(port);
/*
int dst;
inet_pton(AF,INET,"192.233.1.200",(void*)dst)
addr.sin_addr.s_addr=dst;//
*/
addr.sin_addr.s_addr=htonl(INADDR_ANY)//取出系统中有效的任意ip地址
bind((struct sockaddr*)&addr,size);
}
地址转换函数
ip地址可以用点分十进制的字符串如127.0.0.1,也可以用一个32整数来表示,其中就涉及到二者之间 的转换一下是两者相互转化的库函数
字符串转in_addr(32位整数)
#include<arpa/inet.h>
int inet_aton(const char* cp,struct in_addr* inp);
in_addr_t inet_addr(const char* cp);
int inet_pton(int af,const char* src,void* dst);p即ip,n就是网络,将ip中的string转化成为网络字节序,所以返回值是int
这里的af就是ip地址家族,ip协议类型(只能是AF_INET,或者AF_INET6)
src就是我们要转的ip地址(点分是10进制的)dst就是传出参数,就是转化后的网络字节序的ip地址,
返回值:成功返回1,如果返回0说明src里面的字符串并不代表一个有效的ip地址,-1的话就是我们输入的af并非AF_INET,或者说AF_INET6,错误码设置成EAFNOSUPPORT
const char* inet_ntop(int af,const void* src,char* dst,socklen_t size);//将网络字节序转化位ip地址,所以返回值是string
af就是ip协议,
src就是我们要转化的整数,网络字节序中的ip地址
dst就是转换后位字符串的ip地址
socklen_t size就是dst的大小
返回值,成功返回非空字符串指向dst,失败返回null,
errno里面可以查看错误的原因
字符串转整数的函数
inet_addr
函数原型
in_addr_t inet_addr(const char* cp)
参数:
- cp点分10进制的字符串ip
返回值:整数ip
整数转字符串的函数
in_addr转字符串的函数
char * inet_ntoa(struct in_addr in)
in 是描述ip地址的结构体
返回值:字符串IP
const char* inet_ntop(int af,const void* src,char* dst,socklen_t size);//
UDP服务器
server.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include<cstdio>
using namespace std;
int main(int argc,char* argv[])
{
if(argc!=2)
{
cout<<"port"<<endl;
return 1;
}
// 1.创建套接字,打开网络文件,作为一个服务器,要让客户知道对应的服务器的地址,不然就不会访问了
//服务器的socket消息,必须得被客户知道,一般的服务器的port,必须是众所周知的(人,app,),而且轻易不能被改变,
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cerr << "socket create error" << endl;
return 1;
}
// 2.要给服务器绑定服务器的ip和端口号
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1])); //这里面使用的都必须是主机序列
//云服务器不能允许用户直接绑定公网ip,实际正常写的时候,也不会直接指明ip
// local.sin_addr.s_addr=inet_addr("127.0.0.1");//将字符串序列的点分十进制的风格转化为主机序列
local.sin_addr.s_addr = INADDR_ANY; //如果bind是确定的ip(主机),意味着,只有发到该ip主机上的数据,才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个ip,我们需要的是所有发送到该主机
//该端口的数据,我不关心这个数据是从哪个ip上的,只要绑定我这个端口,全部数据我都要,一般我们都是用这个
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind cerror " << errno << endl; //把错误码也加上
return 2;
}
// UDP服务器就写完了
// 3.接下来就是提供服务
bool quit = false;
while (!quit)
{
//和普通文件的读取有差别
//在udp里面读取数据就要使用recvfrom
char buf[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//注意,我们在同行的时候双方是互相发送字符串的,(但是对于文件来说,\0是c,c++的标准),不认为发送字符串
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&peer, &len); //这里面会返回客户端的消息,-1是为了把它当作字符串看待,
if (s > 0)
{
//在网络通信中,只有报文大小,或者是字节流中字节的个数,没有c/c++字符串这样的概念,(后续我们可以自己处理)
buf[s] = 0;
cout << "client # " << buf << endl;
//我们接收的可能是一个命令,服务器执行这个命令,在客户端上面跑
FILE* fp=popen(buf,"r");//把这个命令传进去,我们实现了一个命令行解释器
//读文件
char t[1024];
string ech;
while(fgets(t,sizeof(t),fp))//读到空就结束
{
ech+=t;//里面就是各种内容
}
pclose(fp);
ech += ".....";
//如果我写的udp无法通信,云服务器开放服务,首先需要开放端口,默认的云平台是没有开发特定的端口的,需要所有者,再网络后开放端口
sendto(sockfd, ech.c_str(), ech.size(), 0, (struct sockaddr *)&peer, len);
}
else
{
return 1;
}
}
return 0;
}
client.cc
#include <iostream>
using namespace std;
#include <sys/socket.h>
#include <sys/types.h>
#include <string>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
#include<cstring>
#include<cstdio>
//要知道server对应的ip和port
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "ip+port" << endl;
return 0;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
//这里就不需要再继续绑定了,直接发送数据过去就可以了,不需要显示的bind
// 1.首先客户端也必须要有ip和port
// 2,但是客户端不需要显示bind,一旦显示bind,就必须明确,client和哪一个port进行关联,那么有可能会出现冲突
//有可能被占用,就会导致客户端无法使用,服务器用的是port必须明确,而且不变但client只要有就可以了
//一般是操作系统自动绑定的,就是client正常发送数据的时候,操作系统就会自动绑定
//使用服务
while (1)
{
//发送数据
//数据从哪里来,要给谁发送
string msg;
// cin >> msg;
// getline(cin,msg);
cout<<"Myshell$ ";
char line[1024];
fgets(line,sizeof(line),stdin);//从键盘里面读取
//因为我们要发送的是命令,所以是一整行
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sockfd, line, strlen(line), 0, (struct sockaddr *)&server, sizeof(server));//发送的大小是不包含\0
//接收
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buf[1024];
ssize_t s = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&tmp, &len); // tmp就是一个占位符的概念,里面没有什么用
//会接收到接收的字节数
if (s > 0)
{
//假如说发送的是hello,5个字节
//我们接收端把它当作字符串,就要在最后一个位置加上\0
buf[s] = 0;
cout << buf << endl;
}
else
{
cerr<<"recvfrom error"<<endl;
}
}
return 0;
}
TCP服务器
多进程模式
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include<cstdlib>
#include<signal.h>
#include<string>
#include<sys/wait.h>
using namespace std;
void serverio(int newsockfd)
{
while (true)
{
//因为TCP 是面向字节流的,就如同文件一样,就可以进行正常的读写
char buf[1024];
memset(buf, 0, sizeof(buf));
ssize_t s = read(newsockfd, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0;
cout << "client # " << buf << endl;
string echo = "server send ";
echo += buf;
write(newsockfd, echo.c_str(), echo.size());
}
else if (s == 0)
{
cout << "client quit" << endl; //客户端ctrl c之后就断开了连接
break;
}
else
{
cerr << "error" << endl;
break;
}
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
return 2;
}
// 3.因为tcp是面向连接的,在通信的时候要建立连接,
// a在通信钱,需要建立连接,b:然后才能通信
//一般客户端来建立连接,服务器是被动接收连接
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字为listen状态
if (listen(sockfd, 5) < 0) //设置为被连接状态,这样别人就可以连接到我了
{
cerr << "listen error" << endl;
}
// signal(SIGCHLD,SIG_IGN);//子进程退出的时候会给父进程发送信号,这里我们去忽略一下子进程发送的信号,子进程会自动退出释放资源
while (1)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int newsockfd = accept(sockfd, (struct sockaddr *)&peer, &len);
if (newsockfd < 0)
{
continue; //连接失败就继续连接
}
//我们这里现在是一个单进程,一次只能允许一个人使用
//我们使用多进程来操作一下
uint16_t cli_port=ntohs(peer.sin_port);//保证代码的可移植性
string ip=inet_ntoa(peer.sin_addr);//
cout<<"get a link -> : "<<cli_port<<" : "<<newsockfd<<" "<<ip<<endl;//我们发现每一个都是4
pid_t id = fork();
if (id < 0)
{
continue;
}
else if (id == 0)
//曾经被父进程打开的fd,是否会被子进程继承?无论父子进程的哪一个,强烈建议关闭不需要的fd,监听描述符也会被子进程看到,
//我们在子进程这边把监听描述符关闭
{
// child
close(sockfd);//只是会继承下来但是,关闭不影响其他进程
if(fork()>0)
exit(0);//这个叫做退出的是子进程,向后走的进程是孙子进程
//因为父进程一进来就挂掉了,所以孙子进程就变成了孤儿进程,后会被操作系统给回收掉,
serverio(newsockfd);
//如果不关闭文件描述符,就会导致文件描述符泄露的问题
close(newsockfd);
exit(1);//子进程执行完了,父进程就要等
//子进程和父进程的文件描述符是共享的,退出后要关闭文件描述符
}
else
{
//do nothing ,因为父进程忽略了,所以就不需要去wait,父进程不断的建立连接,子进程不断的提供服务
//阻塞等待不需要等待,耗时间很大,所以使用忽略sigchild信号,
waitpid(id,nullptr,0);//这里等待的时候会不被阻塞等待太长时间,因为它子进程刚一创建出来就退出了孙子进程执行完之后就被OS给回收了
close(newsockfd);//fork已经返回了
}
//提供服务
}
return 0;
}
线程池模式
serverpthread.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <string>
#include <sys/wait.h>
using namespace std;
#include <pthread.h>
#include "threadpool.hpp"
#include "Task.hpp"
using namespace ns_task;
using namespace ns_threadpool;
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return 1;
}
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error" << endl;
return 2;
}
// 3.因为tcp是面向连接的,在通信的时候要建立连接,
// a在通信钱,需要建立连接,b:然后才能通信
//一般客户端来建立连接,服务器是被动接收连接
//我们当前写的是一个server,周而复始的不间断的等待客户到来
//我们要不断的给用户提供一个建立连接的功能
//设置套接字为listen状态
if (listen(sockfd, 5) < 0) //设置为被连接状态,这样别人就可以连接到我了
{
cerr << "listen error" << endl;
}
// signal(SIGCHLD,SIG_IGN);//子进程退出的时候会给父进程发送信号,这里我们去忽略一下子进程发送的信号,子进程会自动退出释放资源
while (1)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int newsockfd = accept(sockfd, (struct sockaddr *)&peer, &len);
if (newsockfd < 0)
{
continue; //连接失败就继续连接
}
//我们这里现在是一个单进程,一次只能允许一个人使用
//我们使用多进程来操作一下
uint16_t cli_port = ntohs(peer.sin_port); //保证代码的可移植性
string ip = inet_ntoa(peer.sin_addr); //
cout << "get a link -> : " << cli_port << " : " << newsockfd << " " << ip << endl; //我们发现每一个都是4
//先构建一个任务
Task t(newsockfd);
// 2.将任务push到后端的线程池就可以了
ThreadPool<Task>::GetInstance()->PushTask(t); //这样就完了//同样这是懒汉模式
//这样使用的话,线程是不会增多的,退出的时候,线程池也不会再变化,这就不会有线程问题
}
return 0;
}
//但是这样去写有两个问题
// 1,创建进程和线程没有上限,进程和线程越多,进程切换就有消耗,时间也就更久了,系统运行的也就特别的慢了,当客户连接来了才给客户创建进程/线程
//进程或者线程池版本,就可以直接用之前的线程池用进去,这里我们结合单例模式
threadpool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
// #cludine"Task.hpp"
// using namespace ns_task;
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool //线程池
{
private:
int num_; //一个线程池里面有多少个任务
std::queue<T> task_queue_; //任务队列,临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
static ThreadPool<T> *ins; //静态成员在所有的对象里面只有一个静态成员,必须要通过静态变量来获取对象
//保存内存的可见性
private:
//单例的话,就不能让构造函数暴露在外面,否则,只有有构造函数,就能初始化
//构造函数必须得实现,当时必须得私有
ThreadPool(int num = g_num) : num_(num)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
ThreadPool(const ThreadPool &tp) = delete;
// c++11的新特性
//静止编译器生成拷贝构造,
//=delete就是禁止调用这个函数,在私有里面
ThreadPool operator=(const ThreadPool &tp) = delete;
//把赋值运算符也禁止掉,这也就可以避免创建多个对象
public:
static ThreadPool<T> *GetInstance() //这个必须是使用静态的,非静态函数都是有对象的,静态函数才是没对象的
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //使用静态的初始化
if (ins == nullptr)//双判定,减少锁的争用,提高单例获取的效率,
//假如说有的线程进来发现不为空,就可以直接走了,如果同时为nullptr的化,那么再把他们放进来争抢锁资源、
{
pthread_mutex_lock(&lock); //争抢锁的过程就是一个串行化的过程,成本很高
//当前的单例对象还没有被创建
if (ins == nullptr)
//假如是在多线程的情况下,那么多个线程执行的时候,都是nullptr,都创建了对象,那么就出现了线程安全
{
//就创建它
ins = new ThreadPool<T>(); //创建一个,使用构造函数
//创建出来了一个单例之后,就直接给他初始化一个池就行了
ins->InitThreadPool();
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
//在类中,要让
static void *Rountine(void *args)
//也不能访问类里面非static成员
{
pthread_detach(pthread_self()); //实现线程分离就不要再去join等待了
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
//从任务队列里面去拿一个任务
//执行任务,要先把这个任务队列锁主
//每个线程他跟放任务的线程一样,都是竞争式的去拿一个任务
tp->Lock();
//先检测任务队列是否有一个任务
while (tp->IsEmpty())
{
//检测到任务队列为空
//此时线程就挂起等待
tp->Wait();
}
//该任务队列里面一定有任务了
T t;
tp->PopTask(&t);
//任务就拿到了
tp->UnLock();
t.Run(); //可能有多个线程在处理任务,
sleep(1);
}
}
void InitThreadPool()
{
//初始化一批线程,
//这样就不要每次用都要去开辟线程了
pthread_t tid; //一次创建一批线程
for (int i = 0; i < num_; i++)
{
pthread_create(&tid, nullptr, Rountine, (void *)this);
//在类中不能执行线程的方法,因为他都有隐藏的this指针
//所以我们需要使用静态的函数,就没有了this指针
}
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
void Wait()
{
pthread_cond_wait(&cond_, &mtx_);
}
bool IsEmpty()
{
return task_queue_.empty();
}
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void UnLock()
{
pthread_mutex_unlock(&mtx_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
void PushTask(const T &in)
{
//塞任务,就相当于一个生产者,生产者之间要进行互斥访问
Lock();
task_queue_.push(in);
UnLock();
Wakeup();
}
//万一任务队列里面一个任务都没有的话,那么线程池里面的每一个线程就要处于休眠状态,挂起等待
};
template <class T>
//静态成员变量的初始化必须要在类外面初始化
ThreadPool<T> *ThreadPool<T>::ins = nullptr; //将threadpool里面的ins进行初始化,返回值是指针,给它初始化为空,说明没有被创建出来
}
Task.cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include"unistd.h"
#include<cstring>
using namespace std;
namespace ns_task
{
class Task
{
private:
int _sockfd;
public:
Task() //无参构造,为了拿任务,不需要参数列表
: _sockfd(-1)
{
}
//进行函数重载
Task(int sockfd)
: _sockfd(sockfd)
{
}
~Task()
{
}
int Run()//执行任务
{
while (true)
{
//因为TCP 是面向字节流的,就如同文件一样,就可以进行正常的读写
char buf[1024];
memset(buf, 0, sizeof(buf));
ssize_t s = read(_sockfd, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0;
cout << "client # " << buf << endl;
string echo = "server send ";
echo += buf;
write(_sockfd, echo.c_str(), echo.size());
}
else if (s == 0)
{
cout << "client quit" << endl; //客户端ctrl c之后就断开了连接
close(_sockfd);
break;
}
else
{
cerr << "error" << endl;
close(_sockfd);
break;
}
}
// close(_sockfd);//跑完了就把套接字关掉就可以了
}
int operator()() //重载一个函数
{
return Run();
}
};
}
popen
执行这个command,后以文件的方式来操作,读还是写操作
执行的结果在FILE* 这个文件指针里面,可以用一个字符串读取里面的内容接收,返回给客户端
执行完关闭
底层是管道,fork,文件方案拿到的
netstate
netstate -nltp
-t是tcp业务
-p是process进程