【Linux】Tcp协议的通讯流程,浅谈三次握手四次挥手

文章目录

  • Tcp协议的通讯流程
  • 一、协议定制与网络版计算器的实现
  • 二、json的使用
  • 总结


Tcp协议的通讯流程

上一篇文章我们讲解了如何实现Tcp服务器,Tcp的接口也用了,下面我们就看一下Tcp协议的通讯流程:

 在服务端,我们首先要创建一个套接字,这个套接字被称为监听套接字,这个时候服务端处理关闭状态。有套接字后我们开始绑定服务端的ip和端口号,然后我们调用listen接口,一旦调用成功我们的服务器由close状态就变为监听状态,一旦变成监听状态看允许客户端来连我们的服务器了,只不过这个时候不获取客户端的链接。监听后我们就可以通过accept接口获取客户端的链接了,这个时候accept接口会给我们返回一个套接字,这个套接字才是真正用于和客户端通信的套接字。

客户端首先创建套接字,然后在发起链接请求的时候会帮我们绑定,connect这个接口是用来建立链接的,而我们只是向服务器发起建立链接的请求,中间三次握手的过程是由双方的操作系统自动来完成的。因为connect是链接建立的发起方,所以我们用accept获取链接的时候一定是链接已经成功建立了我们才能获取。

服务器初始化 :
调用 socket, 创建文件描述符 ;
调用 bind, 将当前的文件描述符和 ip/port 绑定在一起 ; 如果这个端口已经被其他进程占用了 , 就会 bind 失败;
调用 listen, 声明当前这个文件描述符作为一个服务器的文件描述符 , 为后面的 accept 做好准备 ;
调用 accecpt, 并阻塞 , 等待客户端连接过来
建立连接的过程 :
调用 socket, 创建文件描述符 ;
调用 connect, 向服务器发起连接请求 ;
connect 会发出 SYN 段并阻塞等待服务器应答 ; ( 第一次 )
服务器收到客户端的 SYN, 会应答一个 SYN-ACK 段表示 " 同意建立连接 "; ( 第二次 )
客户端收到 SYN-ACK 后会从 connect() 返回 , 同时应答一个 ACK ; ( 第三次 )
注意:三次握手的过程我们会在讲tcp原理的时候详细的讲解,现在只是一些流程和概念。
数据传输的过程:
建立连接后 ,TCP 协议提供全双工的通信服务 ; 所谓全双工的意思是 , 在同一条连接中 , 同一时刻 , 通信双方可以同时写数据; 相对的概念叫做半双工 , 同一条连接在同一时刻 , 只能由一方来写数据 ;
服务器从 accept() 返回后立刻调用 read(), socket 就像读管道一样 , 如果没有数据到达就阻塞等待 ;
这时客户端调用 write() 发送请求给服务器 , 服务器收到后从 read() 返回 , 对客户端的请求进行处理 , 在此期间客户端调用read() 阻塞等待服务器的应答 ;
服务器调用 write() 将处理结果发回给客户端 , 再次调用 read() 阻塞等待下一条请求 ;
客户端收到后从 read() 返回 , 发送下一条请求 , 如此循环下去

断开连接的过程:

如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN(第一次);

此时服务器收到 FIN , 会回应一个 ACK, 同时 read 会返回 0 ( 第二次 );
read 返回之后 , 服务器就知道客户端关闭了连接 , 也调用 close 关闭连接 , 这个时候服务器会向客户端发送一个FIN; ( 第三次 )
客户端收到 FIN, 再返回一个 ACK 给服务器 ; ( 第四次 )

总结:

1.建立链接是双方操作系统自动完成的(三次握手)。

2.建立链接是为了双方维护链接而创建的数据结构,这个数据结构是有成本的,这个成本主要体现在创建要花时间和空间。

3.断开链接(四次挥手)的目的是将曾经建立好的链接信息释放掉


一、协议定制与网络版计算器的实现

之前我们说过,协议是一种约定。那么socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些 "结构化的数据" 怎么办呢?结构化的数据我们可以理解为对象,就像我们的qq,发送消息的时候除了消息还有头像昵称时间,这些数据肯定是不能分开发送给服务器的,因为一旦用户基数很大的时候服务器根本无法区分哪个是哪个的消息。那么如何将这些数据打包成一个发送给服务器呢?要想将多个字节流变成一个字节流,这个过程就叫做序列化。当我们的服务器将这个数据收到后,我们还需要将这个数据恢复为原来没有整合的样子,此时恢复的过程就叫做反序列化。所以:业务结构数据在发送到网络的时候,先序列化再发送收到的一定是序列字节流,要先进行反序列化然后才能使用,这是实现网络协议的第一个细节。我们刚刚说数据发送到网络,但实际上还有一个细节我们没有提到,我们如何确定服务器会完整的收到我们发送的数据呢?这就是我们实现协议的第二个细节。

网络版计算器的实现:

例如 , 我们需要实现一个服务器版的计算器 . 我们需要客户端把要计算的两个加数发过去 , 然后由服务器进行计算 , 最后再把结果返回给客户端。
约定方案:
客户端发送一个形如 "1+1" 的字符串 ;
这个字符串中有两个操作数 , 都是整形 ;
两个数字之间会有一个字符是运算符
数字和运算符之间有间隔
首先我们创建所需要的文件:

之前的Tcp客户端和服务端我们直接拿过来用即可,还有log.hpp等文件,然后我们的Tcp服务端只保留多进程版本:

现在我们要修改的是,如何对数据做处理,首先我们先把函数名改为handerEnter表示处理进入的逻辑,我们将以前的任务文件改名为protocol,也就是协议定制:

 准备工作做完后我们开始进行协议定制,首先我们的协议中包含请求与响应,请求就是客户让我们计算的内容,响应就是我们给出的结果和返回码:

pragma once
#include <iostream>class Request
{
public:public://   "x op y"int x;int y;char op;
};class Response
{
public:int exitcode;   //返回码,0表示计算成功,非0表示计算失败int result;     //计算结果
};

 然后我们写个构造函数初始化一下:

 我们先默认初始化为0,然后开始编写服务器中的回调函数hander:

    void handerEnter(int sock){// 1.读取// 1.1 如何保证读到的消息是一个完整的请求// 2.对请求Request 反序列化// 2.1 得到一个结构化的请求对象Request req =    // 3. 计算机处理req.x  req.y   req.op// 3.1 得到一个结构化的响应Response resp;// 4. 对响应Response进行序列化// 4.1 得到了一个字符串// 5.然后我们再发送响应}

 我们有5个大步骤,首先服务端收到客户端发来的请求(就是让服务器计算某个表达式),我们第一个要保证的是如何能完整的接收客户端发来的数据,而要保证这一点可以有三个步骤(1.数据定长2.特殊符号3.自描述)这三点我们后面会讲到,能保证第一步后我们再对这个请求做去掉报头,然后做反序列化操作,一旦进行反序列化操作我们就能拿到单个的x,y,op,然后我们利用回调函数开始做计算,这个函数我们在server.cc中实现直接传入start函数,计算出来结果后我们还要发回给客户端,所以还需要将结果进行序列化操作,序列化后得到一个“x op y”的字符串,我们将这个字符串发送给客户端即可。因为我们的第3步要得到一个响应,所以我们直接搞一个回调函数:

 

这个回调函数的第一个参数是一个输入型参数,就是客户端发给我们的请求经过反序列化的对象,第二个参数是一个输出型参数,未来我们处理req的时候得到req.x,req.y,req.op然后用他们做计算,将计算结果直接放到resp中。

  void handerEnter(int sock, func_t func){// 1.读取// 1.1 如何保证读到的消息是一个完整的请求// 2.对请求Request 反序列化// 2.1 得到一个结构化的请求对象Request req =    // 3. 计算机处理req.x  req.y   req.op// 3.1 得到一个结构化的响应Response resp;func(req,resp); //req的处理结果全部放入了resp中// 4. 对响应Response进行序列化// 4.1 得到了一个字符串// 5.然后我们再发送响应}

 有了这个回调函数,那么我们在server.cc中就直接可以根据处理好的req去填充resp:

 那么如何保证读到的消息是一个完整的请求呢?因为tcp是面向字节流的,所以我们可以明确报文和报文的边界,明确报文和报文边界有三种方式:1.定长 2.特殊符号 3.自描述,如下图:

 tcp内部是有自己的发送缓冲区和接收缓冲区的,所以我们只需要规定每个报文的大小的都是一样的,这样就明确了报文和报文边界,当然我们也可以在报文中加一个特殊的符号,用这些符号去表示报文边界,那么自描述是什么呢,其实就是我们可以规定报文的前四个字节是什么什么,后面是什么什么。(我们今天所演示的自描述就是添加报头,这个报头可以是符号也可以代表一些含义)

有了以上概念我们就可以写序列化与反序列化函数了,未来这两个函数一定是能让req和resp使用的,所以我们先给req这个类实现序列化与反序列化函数:

#define SEP " "
#define SEP_LEN strlen(SEP)bool serialize(std::string *out){*out = "";std::string x_string = std::to_string(x);std::string y_string = std::to_string(y);*out += x_string;*out += SEP;*out += op;*out += SEP;*out += y_string;return true;}

首先我们的规定是序列化后的字符串是: "x op y"的形式,所以我们将分隔符定义为空格,然后定义空格符的长度,这样以后更改会很方便。首先对out指针做初始化,因为我们不能保证给我们传的字符串是空字符串,然后我们将x和y转化为string类型让out字符串先+x,再加分隔符,再加运算符,再加分隔符最后再加y即可,这样外部的out参数就得到了序列化的数据,然后我们返回true。

bool deserialize(const std::string& in){auto left = in.find(SEP);auto right = in.rfind(SEP);if (left==std::string::npos || right==std::string::npos){return false;}if (left == right){return false;}if (right - (left+SEP_LEN) != 1){return false;}std::string x_string = in.substr(0,left);std::string y_string = in.substr(right+SEP_LEN);if (x_string.empty()) return false;if (y_string.empty()) return false;x = stoi(x_string);y = stoi(y_string);op = in[left+SEP_LEN];return true;}

反序列化也很简单,我们首先找到字符串中左边的空格和右边的空格,然后判断是否满足我们要求的格式,不满足直接返回false。满足后我们就获取序列化字符串中的x,y,op得到后保存在类内的成员变量中,然后返回true。

有了对请求的序列化和反序列化我们就可以计算了:

bool cal(const Request& req,Response& resp)
{//req已经有结构化完成的数据了,可以直接使用resp.exitcode = OK;resp.result = OK;switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':{if (req.y==0){resp.exitcode = DIV_ZERO;}else {resp.result = req.x / req.y;}}break;case '%':{if (req.y==0){resp.exitcode = MOD_ZERO;}else {resp.result = req.x % req.y;}}break;default:resp.exitcode = OP_ERROR;break;}return true;
}

 计算也很简单,首先将回应类中的返回码和结果初始化,然后利用switch判断运算符(注意:switch语句中是可以在case语句后面加大括号的,但是我们的break一定要写在大括号外面,否则就犯了C语言的错误),对于除0错误我们可以在协议头文件中设置一个错误码:

enum
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};

上面我们只是完成了请求的序列化和反序列化,我们还需要对结果的相应也做序列化与反序列化:

    //"exitcode result"bool serialize(std::string* out){*out = "";*out+= to_string(exitcode);*out += SEP;*out+= to_string(result);return true;}bool deserialize(const std::string& in){auto pos = in.find(SEP);if (pos==string::npos){return false;}exitcode = stoi(in.substr(0,pos));result = stoi(in.substr(pos+SEP_LEN));return true;}

因为我们规定序列化的结果必须是"exitcode result"的形式,所以响应的序列化中只需要把返回码和空格和结果拼接起来即可。反序列化也很简单,就是先找到空格,然后通过空格分割出返回码和结果的字符串,然后将字符串转化为int赋值给响应类的私有成员即可,这样响应类中就保存了计算出的结果和返回码。

下面我们再写添加报头的函数,由于这个函数响应和请求都需要,所以我们直接写在协议头文件中:

#define SEP " "
#define SEP_LEN strlen(SEP)#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //  自定义报头:"x op y" -> content_len\r\n"x op y"\r\n
//             "exitcode result" -> content_len(正文长度)\r\n"exitcode result"\r\n
const string enLength(const std::string& text)   //添加报头
{string send_string = to_string(text.size());send_string+=LINE_SEP;send_string+=text;send_string+=LINE_SEP;return send_string;
}

我们规定一个完整的报文必须是:"x op y" -> content_len\r\n"x op y"\r\n这样的格式,就是正文长度 + \r\n(特殊符号)+正文 + \r\n,而这个函数给我们传的参数一定是正文"x op y"这样的字符串,所以我们只需要计算正文长度,然后将正文长度转化为字符串,将正文长度加上特殊符号加上正文加上特殊符号,这样就是我们规定的报文了,然后返回即可。

有了添加报头我们还需要去掉报头的函数:

bool deLength(const std::string& package,string* text)
{auto pos = package.find(LINE_SEP);if (pos==string::npos){return false;}int len = stoi(package.substr(0,pos));*text = package.substr(pos+LINE_SEP_LEN,len);return true;
}

package参数就是一个完整报文,text是输出型参数,就是我们将去掉报头的结果放在text中返回给外界。去掉报头也很简单,先找到特殊符号,然后我们获取正文的长度,注意:我们规定完整的报文开头到第一个\r\n就是报文长度。有了报文长度后直接用substr函数就拿到了正文。拿到后返回true即可。

下面我们完善hander函数:

 void handerEnter(int sock, func_t func){while (true){// 1.读取// 1.1 如何保证读到的消息是一个完整的请求std::string req_str;//?????????cout<<"处理前请求的完整报文:\n"<<req_str<<endl;// 如果走到这里,那么可以保证req_str得到的是一个完整的请求string ret;if (!deLength(req_str, &ret)){return;}cout<<"去掉报头的请求报文:\n"<<ret<<endl;// 2.对请求Request 反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(ret)){// 反序列化失败就returnreturn;}// 3. 计算机处理req.x  req.y   req.op// 3.1 得到一个结构化的响应Response resp;func(req, resp);// 4. 对响应Response进行序列化// 4.1 得到了一个字符串std::string resp_str;resp.serialize(&resp_str);cout<<"进行序列化后的响应的字符串:\n"<<resp_str<<endl;// 5.然后我们再发送响应// 5.1构建成为一个完整的报文string send_string = enLength(resp_str);cout<<"对序列化后的响应添加报头的字符串:\n"<<send_string<<endl;send(sock, send_string.c_str(), send_string.size(), 0);}}

因为我们是多次给客户端发送响应,所以这里应该是个循环。然后我们除了没有保证读到的消息是一个完整的报文,其他的阶段都搞定了。假设我们已经读到一个完整的报文了,然后对请求做去掉报头操作,去掉报头后再进行反序列化操作,这样就拿到了x,y,op并且保存在req中,然后我们创建一个响应对象,调用回调函数将req中x,y,op的结果计算出来,把结果和返回码填到resp中。然后我们对resp中的返回码和结果进行序列化操作,序列化后添加报头就可以发送给客户端了,下面我们编写保证得到的是完整报文的函数:

bool recvPackage(int sock, string& inbuffer, string *text)
{//因为是从sock中读取数据放到text中,所以为了保险先对text清空(*text).clear();char buffer[1024];while (true){ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);if (n>0){buffer[n] = 0;inbuffer+=buffer;//分析处理得到一个完整的报文auto pos = inbuffer.find(LINE_SEP);if (pos==string::npos){//没找到分隔符,一定不是完整的报文,需要继续往inbuffer中读continue;}int text_len = stoi(inbuffer.substr(0,pos));int connumber_len = to_string(text_len).size();int total_len = connumber_len + text_len + 2*LINE_SEP_LEN;if (total_len > inbuffer.size()){continue;}//inbuffer.size()大于一个完整报文长度,那么inbuffer中至少有一个完整的报文*text = inbuffer.substr(0,total_len);inbuffer.erase(0,total_len);//读到一个完整的报文就退出break;}else{return false;}}return true;
}

首先我们要从文件描述符中读到完整的报文放在text中,所以我们保险起见先将text清空。inbuffer参数是一个支持我们持续存放报文的字符串,因为我们读取是有可能读不到一个完整的报文的,所以这些暂时用不到的报文都会放在inbuffer中。所以我们保险起见先将text清空。然后我们定义一个

缓冲区,这个缓冲区会存放我们从文件描述符中读出来的数据,这里我们用recv接口读取,返回值与read一样都是读到的字节数。如果n==0说明客户端退出,这个时候我们返回false即可。如果n大于0,说明读取成功,然后我们在缓冲区最后一个位置加上\0然后把数据加到inbuffer中,注意是+=因为会持续的读取。然后我们在inbuffer中查找特殊字符\r\n,如果没找到那么一定不会有完整的报文,所以我们continue继续读取,读取到特殊符号后,我们首先保存正文长度和正文数字的长度(比如 4\r\nhoow\r\n),首先正文的长度是4,前面这个4的长度是1,1就是正文数字的长度。有了这两个长度我们就能计算一个完整报文的长度,完整报文 = 正文数组长度 + 2*特殊字符长度 + 正文长度。然后我们判断inbuffer中的长度是否大于完整报文的长度,如果不大于则说明inbuffer没有完整报文需要继续读取,如果有完整报文我们就用substr函数拿到完整报文,并且将inbuffer中刚刚拿掉的报文删除,这样就不会影响下一个报文。注意:我们读到一个报文就退出处理。

 然后我们的hander方法就全部搞定了,注意在hander中定义一个inbuffer用来存放循环读取到的客户端报文。

服务端我们搞定后,现在去搞定客户端,让客户端发送请求:

void start(){struct sockaddr_in server;bzero(&server,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());//connet的时候操作系统会帮客户端bind   返回值等于0成功if (connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0){cerr<<"socket connect error"<<endl;}else {string msg;string inbuffer;while (true){cout<<"mycal>> ";getline(cin,msg);//将字符串中转换为一个请求}}}

我们客户端肯定是输入要计算的字符串,那么我们如何将字符串转化为一个响应呢?其实很简单,我们规定只是"1+1" 或者 "10/12"这样的格式:

 Request ParseLine(const string& line){//规定表达式  "1+1" " "12/0"int status = 0;  //status是一个标志,0表示操作符之前,1表示操作符,2表示操作符之后int i = 0;int cnt = line.size();string left,right;char op;while (i<cnt){switch (status){case 0:{if (!isdigit(line[i])){op = line[i];status = 1;}else {left.push_back(line[i++]);}}break;case 1:i++;status = 2;break;case 2:right.push_back(line[i++]);break;}}return Request(stoi(left),stoi(right),op);}

我们实现的原理很简单,首先大循环是遍历字符串,在遍历的过程中我们设置一个标志,标志为0代表这个字符属于左操作数,标志为1代表属于运算符,标志为2代表属于右操作数。然后我们通过switch语句去判断,最后拿到左操作数和右操作数和运算符,然后我们要想构造请求对象还必须给request类多加一个构造函数:

 同样如果响应类也需要的话,我们也加一个。

有了构造函数我们直接匿名对象返回即可,这样我们就将一个字符串转换为请求对象了。

void start(){struct sockaddr_in server;bzero(&server,sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_serverport);server.sin_addr.s_addr = inet_addr(_serverip.c_str());//connet的时候操作系统会帮客户端bind   返回值等于0成功if (connect(_sock,(struct sockaddr*)&server,sizeof(server))!=0){cerr<<"socket connect error"<<endl;}else {string msg;string inbuffer;while (true){cout<<"mycal>> ";getline(cin,msg);//先有结构化的数据,然后序列化后发送给服务端Request req = ParseLine(msg);//得到序列化的结果string req_s;req.serialize(&req_s);//给序列化的结果添加报头string send_string = enLength(req_s);send(_sock,send_string.c_str(),send_string.size(),0);//读取服务器返回的序列化结果string total_text;if (!recvPackage(_sock,inbuffer,&total_text)){continue;}//走到这total_text中一定是一个完整的报文Response resp;string text;if (!deLength(total_text,&text)){continue;}//走到这text中一定是一个去掉报头的报文resp.deserialize(text);cout<<"exitCode: "<<resp.exitcode<<endl;cout<<"result: "<<resp.result<<endl;}}}

然后我们从刚刚那一步开始,有了请求对象后我们需要对这个请求做序列化操作,序列化后添加报头,然后发送给服务器。同时我们还要接受服务器发给我们的相应,所以我们继续使用recvPackage函数读取一个完整的报文,如果我们读取失败那么就继续读取,直到读取成功我们将这个报文去除报头,然后进行反序列化操作这样resp中就保存了返回码和结果,然后我们将结果打印出来,下面我们运行起来演示一下:

 可以看到我们的网页版计算器是没问题的,对于除0错误我们只关心错误码。这样我们就完成了网页版计算器。

二、json的使用

对于上面我们自己写的序列化和反序列化操作,实际上在日常生活中我们是不用的,一般都是用现成的,下面我们讲一下最简单的json c++版本的使用:

首先linux安装json:

yum install -y jsoncpp-devel

注意没有权限的用sudo提权。

class Request
{
public:Request():x(0),y(0),op(0){}Request(int _x,int _y,char _op):x(_x),y(_y),op(_op){}bool serialize(std::string *out){Json::Value root;root["first"] = x;root["second"] = y;root["oper"] = op;Json::FastWriter writer;*out = writer.write(root);return true;}bool deserialize(const std::string& in){Json::Value root;Json::Reader read;read.parse(in,root);x = root["first"].asInt();y = root["second"].asInt();op = root["oper"].asInt();return true;}
public://   "x op y"int x;int y;char op;
};

json的使用非常简单,首先定义一个万能的Value,然后因为是做序列化操作,所以需要将我们的x,y,op和root中进行一个映射,json使用的是键值对方式,所以我们就让x和first做一个映射,y和second做一个映射,op同理,做好映射我们定义一个Fastwriter对象,注意writer就是json中用来序列化的,然后writer中的write函数可以直接将root进行序列化并且返回的是一个字符串。

反序列化同样定义万能的Value,然后json中reader是进行反序列化的,我们利用read中的parse函数,这个函数第一个参数是从哪个流进行反序列化,第二个参数就是我们的root。然后我们就可以将root中的x,y,op都拿出来,因为root中是字符串类型,而我们要的是整形,所以用asInt转化为整形,op并不会受影响,因为char的本质是ascll值。

response的序列化和反序列化同理,大家可以试试:

class Response
{
public:Response():exitcode(0),result(0){}Response(int exitcode_,int result_):exitcode(exitcode_),result(result_){}bool serialize(std::string* out){Json::Value root;root["exitcode"] = exitcode;root["result"] = result;Json::FastWriter writer;*out = writer.write(root);return true;}bool deserialize(const std::string& in){Json::Value root;Json::Reader read;read.parse(in,root);exitcode = root["exitcode"].asInt();result = root["result"].asInt();return true;}
public:int exitcode;   //返回码,0表示计算成功,非0表示计算失败int result;     //计算结果
};

下面我们运行起来看看json的效果:

 可以看到换上json我们的代码也是没有问题的,我们在json和自己写的直接可以用条件编译,这样的话就可以想用哪个用哪个:

 然后我们的makefile也可以修改一下,对于条件编译,我们可以直接编译的时候添加MYSELF,这样就会用我们自己写的序列化和反序列化代码。

 红框的部分效果是一样的,只不过用LD变量会方便切换json和我们自己写的序列化和反序列化代码。

以上就是网页版计算器的全部内容了。


总结

自定义协议就是定义一个结构化的对象,有了结构化的对象未来客户端和服务端就可以来回进行发送和接收,约定在上面的代码中体现在规定必须是"x op y"这样的格式,这就是约定。注意:没有规定我们网络通信的时候只能有一种协议,实际上我们完全可以将上述代码中规定前面是正文长度替换为协议编码,通过协议编码就可以选择要执行哪种协议了。

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

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

相关文章

电脑C盘空间大小调整 --- 扩容(扩大/缩小)--磁盘分区大小调整/移动

概述&#xff1a; 此方法适合C盘右边没有可分配空间&#xff08;空闲空间&#xff09;的情况&#xff0c;D盘有数据不方便删除D盘分区的情况下&#xff0c;可以使用傲梅分区助手软件进行跨分区调整分区大小&#xff0c;不会损坏数据。反之可直接使用系统的磁盘管理工具进行调整…

Flutter学习—— Vscode创建项目

目录 一、Vscode创建项目 二、补充五种项目类型 Application: Module 模块开发&#xff0c; Package开发 Plugin 插件开发 Skeleton 骨架开发 一、Vscode创建项目 1.快捷键 CtrlShiftP 打开命令面板&#xff0c;选择新项目 2.选择需要开发的项目类型 Application 应用开…

Tomcat 的使用(图文教学)

Tomcat 的使用&#xff08;图文教学&#xff09; 前言一、什么是Tomcat&#xff1f;二、Tomcat 服务器和 Servlet 版本的对应关系三、Tomcat 的使用1、安装2、目录介绍3、如何启动4、Tomcat 的停止5、如何修改 Tomcat 的端口号6、如何部暑 web 工程到 Tomcat 中6.1 方式一6.2 …

红队打靶:Nullbyte打靶思路详解(vulnhub)

目录 写在开头 第一步&#xff1a;主机发现与端口扫描 第二步&#xff1a;Web渗透 第三步&#xff1a;hydra密码爆破 第四步&#xff1a;SQL注入大赏 方法一&#xff1a;手工SQL注入之联合查询 方法二&#xff1a;SQL注入写入一句话木马 方法三&#xff1a;SQL注入写入…

C语言学习笔记 VScode设置C环境-06

目录 一、下载vscode软件 二、安装minGW软件 三、VS Code安装C/C插件 3.1 搜索并安装C/C插件 3.2 配置C/C环境 总结 一、下载vscode软件 在官网上下载最新的版本 Download Visual Studio Code - Mac, Linux, Windowshttps://code.visualstudio.com/download 二、安装minGW…

测试覆盖率 JVM 字节码测试运用 - 远程调试、测试覆盖、影子数据库

目录 前言&#xff1a; 简介 基础使用方式介绍 工具特性 前言&#xff1a; 在软件开发中&#xff0c;测试覆盖率是一个非常重要的指标&#xff0c;它表示代码中所有的测试用例是否都已经被覆盖到。JVM 字节码测试是一种比较新的测试方法&#xff0c;它可以对 JVM 字节码进…

nlp系列(6)文本实体识别(Bi-LSTM+CRF)pytorch

模型介绍 LSTM&#xff1a;长短期记忆网络&#xff08;Long-short-term-memory&#xff09;,能够记住长句子的前后信息&#xff0c;解决了RNN的问题&#xff08;时间间隔较大时&#xff0c;网络对前面的信息会遗忘&#xff0c;从而出现梯度消失问题&#xff0c;会形成长期依赖…

开源项目注意事项

fork项目后&#xff0c;记得另外开启一个分支然后在新分支上进行开发&#xff0c;push到仓库后从分支往原项目提交。 否则会出现Partially verified&#xff08;导致提交pr后auto-merge失败&#xff09; 注意git提交操作 https://blog.csdn.net/sonichenn/article/details/13…

flask中的werkzeug介绍

flask中的werkzeug Werkzeug是一个Python库&#xff0c;用于开发Web应用程序。它是一个WSGI&#xff08;Web Server Gateway Interface&#xff09;工具包&#xff0c;提供了一系列实用功能来帮助开发者处理HTTP请求、响应、URLs等等。Werkzeug的设计非常灵活&#xff0c;可以…

请问学JavaScript 前要学html 和css 吗?

前言 html和css可以理解为是一个网站的骨架和皮肤&#xff0c;这两部分做好后整个网站的外观展示的完成度基本就有了个90%左右&#xff0c;所以在学习js前是需要学习html和css 的&#xff0c;这两部分不用花特别多的时间&#xff08;虽然css如果想做一些非常炫酷的效果个人认为…

vue中重新获取数据导致页面加长,要求在页面更新之后浏览器滚动条滚动到之前浏览记录的位置。以及获取当前页面中是哪个元素产生滚动条的方法。

目前的页面样式为&#xff1a; 代码是&#xff1a; <section id"detailSection"><el-tableref"multipleTable":data"logDetailList"style"width: 650px;margin:20px auto;"id"dialogDetail":show-header"fals…

App测试流程及测试点

1 APP测试基本流程 1.1流程图 1.2测试周期 测试周期可按项目的开发周期来确定测试时间&#xff0c;一般测试时间为两三周&#xff08;即15个工作日&#xff09;&#xff0c;根据项目情况以及版本质量可适当缩短或延长测试时间。正式测试前先向主管确认项目排期。 1.3测试资源…

启动es容器错误

说明&#xff1a;启动es容器&#xff0c;刚启动就停止&#xff0c;查看日志&#xff0c;出现以下错误信息&#xff08;java.lang.IllegalArgumentException: Plugin [analysis-ik] was built for Elasticsearch version 8.8.2 but version 7.12.1 is running&#xff09; 解决&…

【2023】HashMap详细源码分析解读

前言 在弄清楚HashMap之前先介绍一下使用到的数据结构&#xff0c;在jdk1.8之后HashMap中为了优化效率加入了红黑树这种数据结构。 树 在计算机科学中&#xff0c;树&#xff08;英语&#xff1a;tree&#xff09;是一种抽象数据类型&#xff08;ADT&#xff09;或是实作这种…

数据结构【栈和队列】

第三章 栈与队列 一、栈 1.定义&#xff1a;只允许一端进行插入和删除的线性表&#xff0c;结构与手枪的弹夹差不多&#xff0c;可以作为实现递归函数&#xff08;调用和返回都是后进先出&#xff09;调用的一种数据结构&#xff1b; 栈顶&#xff1a;允许插入删除的那端&…

网络知识点之-BGP协议

边界网关协议&#xff08;BGP&#xff09;是运行于 TCP 上的一种自治系统的路由协议。 BGP 是唯一一个用来处理像因特网大小的网络的协议&#xff0c;也是唯一能够妥善处理好不相关路由域间的多路连接的协议。 BGP 构建在 EGP 的经验之上。 BGP 系统的主要功能是和其他的 BGP 系…

特征选择策略:为检测乳腺癌生物标志物寻找新出口

内容一览&#xff1a;microRNA&#xff08;小分子核糖核酸&#xff09;是一类短小的单链非编码 RNA 转录体。这些分子在多种恶性肿瘤中呈现失控性生长&#xff0c;因此近年来被诸多研究确定为确诊癌症的可靠的生物标志物 (biomarker)。在多种病理分析中&#xff0c;差异表达分析…

vue3下的uniapp跨域踩坑

uniapp vue3 H5跨域踩坑 开发移动端H5的时候由于后端接口没有做跨域处理&#xff0c;因此需要做下服务器代理&#xff0c;于是百度搜索了uniapp下h5的跨域配置 在manifest下的h5配置proxy&#xff0c;大概是这样: "h5": {"devServer": {"https"…

安全—01day

文章目录 1. 编码1.1 ASCLL编码1.2 URL编码1.3 Unicode编码1.4 HTML编码1.5 Base64编码 2. form表单2.1 php接收form表单2.2 python接收form表单 1. 编码 1.1 ASCLL编码 ASCII 是基于拉丁字母的一套电脑编码系统&#xff0c;主要用于显示现代英语和其他西欧语言。它是最通用的…

ajax/axios访问后端测试方法

文章目录 1、浏览器执行javascript方法GET请求POST请求 2、Postman测试工具GET请求POST请求 3、idea IDE提供的httpclient4、Apache JMeter 1、浏览器执行javascript方法 GET请求 http://localhost:6060/admin/get/123 POST请求 技巧&#xff1a;打开谷歌浏览器&#xff0c…