根据本系列上两篇关于网络的初识介绍,现在我们开始实现一个UDP接口,以加强对该接口的理解。

1 . 服务器端 

在本篇中,主要按照下面内容来实现:

创建并封装服务端:了解创建服务端的基本步骤
创建并封装客户端,测试客户端和服务端通信:了解创建客户端的基本步骤和二者通信
测试云服务器与本地进行通信:从本地通信到真正实现网络通信
根据上面的内容,本次设计的服务器功能就是接受客户端发送的信息并向客户端返回服务端收到的信息

老规矩,先设计整体的makefile,目的是生成两个可执行文件。

CC=g++
LDFLAGE=-o
FLAGE=-std=c++17 -lpthreadServer_src=UDP_Server.cc
Client_src=UDP_Client.cc# SRC=UDP_Client.cc UDP_Server.cc
# OBJ=$(SRC:.cc=.o)Server=UDP_Server
Client=UDP_Client.PHONY:all
all:$(Server) $(Client)$(Server):$(Server_src)@$(CC) $^ $(LDFLAGE) $@ $(FLAGE)$(Client):$(Client_src)@$(CC) $^ $(LDFLAGE) $@ $(FLAGE)@echo "compilation success".PHONY:clean
clean:@rm -rf $(Server) $(Client)@echo "clean done"

 然后搭建Server端的框架:

服务器端首先肯定需要被初始化,然后再永不停息的start起来(预判里面可能会使用while(1)的死循环)

1.1 socket sockaddr 

什么是socket套接字:

  • 套接字是通信的端点,它是一种用于在网络上进程间通信的机制。可以把它想象成一个管道或者接口,应用程序通过这个管道可以发送和接收网络数据。

  • 套接字是操作系统提供的一种抽象概念,它屏蔽了底层网络通信的复杂细节,使得程序员可以方便地进行网络编程。例如,当我们在浏览器中访问一个网页时,浏览器和网页服务器之间就通过套接字进行数据传输,包括请求网页内容和返回网页数据等操作。

现在就要正式开始对服务器的相关信息、接口进行设置。首先需要创建socket文件描述,可以使用socket接口,该接口原型如下:

int socket(int domain, int type, int protocol);

首先来学习socket接口及其参数,先把两个头文件加上去。

                

        第一个参数表示这个socket套接字想选择的域或者协议家族(domain),domain(域) 是套接字的一个重要属性,它指定了套接字所使用的协议族(protocol family)。协议族决定了套接字能够与哪些类型的网络地址进行通信(不同类型的网络地址遵循不同的协议,以下是常用的协议族:

AF_UNIX AF_LOCAL都是用于本地通信的协议族,AF_INET AF_INET是适用于网络的协议族,前者适应的是IPv4,后者是IPv6。

第二个参数type:

比如常见的,TCP是面向字节流的传输方式,type选SOCK_STREAM ; UDP是数据包传送方式,选择SOCK_DGRAM。可以说,我们今天是在实现UDP,所以先认识了UDP的特性——事实上,是SOCK_DGRAM的特性决定了UDP的特性

根据Linux手册描述:

  1. SOCK_STREAM:Provides sequenced, reliable, two-way, connection-based byte streams(提供序列化的、可靠的、双工的、面向有连接的字节流)

  2. SOCK_DGRAM:Supports datagrams (connectionless, unreliable messages of a fixed maximum length)(支持数据包,即无连接、不可靠的固定长度信息)
     

通信模式可分为:

  1. 全双工:双方同时收发,如同电话通话。

  2. 半双工:双方均可收发,但同一时间只能一方传输,类似对讲机。

  3. 单工:数据单向传输,如电视台广播。

第三个参数

第三个参数表示指定采用的具体协议。通常传入0表示让系统自动选择适合domaintype参数的默认协议

返回值

该接口返回值为一个新套接字的文件描述符(LINUX下一切皆文件,套接字也是一个文件),否则返回-1并设置错误码

理解socket和sockaddr的关系:

        初始化一个socket后,我们已经有了这样一个endpoint for communication(通信端点),但是需要把这个用文件描述符描述的通信端点 绑定 到一个具体的网络地址(网络上的一个地址,包括IP地址和端口号等信息。,所以我们还要绑定现在这个socket到我们具体的sockaddr上去。

        地址多种多样,可能是AF_INET的地址,也可能是AF_UNIX的地址。

本来AF_INET是用socketaddr_in标记,AF_UNIX用socketaddr_un标记,但是为了实用性把他们两个给继承到了一个统一的socketaddr里,所以就变成了socketaddr来标记所有的套接字,在使用的时候通过强转来找到对应的结构。

在逻辑上再次理解domain域和type 

由AF_INET+SOCK_DGRAM形成的UDP 

由AF_UNIX+TCP形成的本地通信。 

         

 代码实现:

 void InitServer(){ENABLE_FILE_LOG;//日志文件使能//1.创建套接字int sock_fd = ::socket(AF_INET,SOCK_DGRAM,0);if(sock_fd<0){Die(1);LOG(LogLevel::FATAL)<<"socket: "<<strerror(errno);}//创建成功,看看套接字LOG(LogLevel::INFO)<<"socket success , socket fd is :"<<sock_fd;//2.填充网络信息并bind}

关于LOG:【LINUX操作系统】日志系统——自己实现一个简易的日志系统-CSDN博客

可以转到socket参数的宏中去看一下:

                

        

1.2 关于bind

该接口的第一个参数表示需要绑定的套接字对应的文件描述符,第二个参数表示套接字结构,第三个参数表示套接字结构的大小

        如果绑定成功,该接口返回0,否则返回-1并设置错误码

        对于第一个参数来说,就是希望被绑定的套接字;第三个参数表示传入的第二个sockaddr的长度,因为第二个参数一般情况下存在强转,需要知道具体这个信息标签有多长。

下面就第二个参数详细介绍:

        在[Socket编程基础]部分提到sockaddr可以理解为sockaddr_in结构和sockaddr_un的父类,而因为本次创建的是网络通信,所以要使用的结构就是sockaddr_in(in后缀表示inet,un后缀表示unix),既然参数部分是sockaddr结构而不是sockaddr_in,那么在传递实参时就需要进行强制类型转换。

简单实现一个强转的宏

#define CONV(addr) (const sockaddr*)(addr) 

现在,我们已经在自己的函数栈上创建好了套接字,也完成了绑定。唯独就是in_addr的信息还没有绑定。

        那么,既然需要用户传递sockaddr_in结构,那么这个结构中就存在一些属性需要用户去设置————地址类型、端口号、IP地址共三种字段需要我们去设置。

                                                

注意,此时我们还在“自己和自己玩”,还和其他机器通信毫无关系。sockaddr的这个“派生类”描述的都是自己这个套接字的信息!填充位留在那里就好了

观察一下sockaddr_in(红字中的派生类)的结构,知道结构才能赋值。

其中关于sin(sock inet)有一个宏,结构如下:

        

传进去的是sin,##表示将两边的符号相结合,所以第一个宏定义的参数其实是sin_family

,表示协议簇。也就是我们说的domain。

明明socket的时候都填了一次,怎么还要填一次?

创建套接字的时候域或者协议簇选择一次(告诉操作系统套接字类型),sockaddr_in内部再赋值一次,两个要一样才能让操作系统绑定上!

最后,关于第三个结构体sin_addr

                        

C语言不支持结构体直接整体赋值,因此我们需要内部数据一个一个赋值,不过此处的in_addr只有一个数据,就很方便直接赋值。

作为服务器,自己的端口号和IP地址肯定是会被知道的:

 

注意,socketaddr_in还需要引入头文件<netinet/in.h> <arpa/inet.h>,结合前面两个,构成网络四大头文件             ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​                                              

可以用指令man inet_addr查到。

不过可不能简单把_port写进去,还需要满足网络字节序

        ​​​​​​​         

        IP用十进制点分法的string表示只是方便观察,但内核存储不应该是4字节?所以肯定要想办法改变。既要从字符串变成一个uint32_t的数据,还要符合网络字节序列,只使用一个简单的htonl(host to net long)肯定是不现实的。

        新的接口 inet_addr:直接将一个const char*的东西返回成一个符合网络字节序和规则的IP地址。

 inet_addr_t是in_addr的返回值,也就是IP在网络字节序的存在形式,也是

也是对应结构体的数据类型

        ​​​​​​​        ​​​​​​​        

    

现在只需要我们把初始的port和IP设置进来即可。 

      

127.0.0.1 是一个特殊的 IPv4 地址,称为 回环地址(Loopback Address)。它用于标识本地计算机本身:

  • 127.0.0.1 用于在本地计算机上进行网络通信,而无需通过外部网络。

(相当于这个IP发出去时:自己的网卡放出去然后不进入网络,直接又自己的网卡接受信息)

  • 它常用于测试和调试网络应用程序,确保程序能够在本地环境中正常运行。

选择端口号8080,这是一个不会被使用的端口(0-1023都是绑定好的协议端口号)

为什么sockaddr需要有端口号和IP?

        因为报文是需要返回的,返回的时候需要知道是哪个socket发出来的(寄包裹总得知道是谁寄出去的)

        所以报头必须有原IP和原端口号,服务器一定会把这两个信息也推送给对方。

现在终于完成了bind。bind之后,这个套接字才算设置进内核中

最后,在进行填数据之前,因为有各种占位的原因,建议先把这个sockaddr_in清空。

 


1.3 Start

                   

Start的整体思路:一个isrunning的状态标记是否启动,然后需要在一个while(true)下面,不停的通过recvfrom接口和sendto接口以收发消息(还需要一个缓冲数组首发传的消息)。(这是一个全双工的接口,支持又读又写)

        ​​​​​​​        

在while true里进行这个接受。

1.4 recvfrom 与 sendto

recvfromsendto 是两个用于网络编程的系统调用,它们是针对数据报套接字(如使用 SOCK_DGRAM 类型的套接字)进行通信的接口。

src_addr addrlen都是输出型参数,recvfrom作为接受方,需要知道谁来连接了我们这个socket(收到包裹的时候,都会有一个快递单号,上面写了谁寄出来的)。flags设置为0表示是阻塞式接受,所以如果我们设置为0,进程会卡在原地等待此次网络通信。

sendto:

两者的返回值都是成功接受/发送的长度

想从服务器回消息到主机去,就需要一个sendto接口回消息。

  void Start(){_isrunning = true;while(true){char inbuffer[SIZE];sockaddr_in* peer;socklen_t peer_len = sizeof(peer);int n =  recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(peer),&peer_len);if(n>0){std::string echo = "#echo: ";LOG(LogLevel::DEBUG)<<inbuffer;echo += inbuffer;int n = sendto(_sockfd,echo.c_str(),echo.length(),0,CONV(peer),peer_len);}}_isrunning = false;}

顺便修正一下之前的一个小bug:之前是直接使用的指针,没有开辟空间。

 下次别这么玩了。直接sockaddr_in一个该多好,整的又是段错误又是绑定失败的..........

补充:可以说,recvfrom和sendto的最后两个参数都是去描述通信的另一端的,前面的sock_fd都是描述自己这一端的 

验证实验成果: 

结合指令:netstat -unap 

netstat 是一个非常有用的网络工具,用于显示网络连接、路由表、接口统计信息、伪装连接以及多播成员信息等。


2. 客户端

        服务器是被别人联的,所以刚刚的而客户端的peer根本不需要知道是谁,直接当作输出型参数即可。而客户端必须知道服务器的IP和端口,客户端需要主动给服务器发数据,去申请内容。

这样的模式称之为CS(Client Server )模式:

        CS模式是一种基本的网络通信模型,它定义了客户端和服务器之间的通信关系。这种模式在现代网络应用中非常普遍,因为它提供了一种可靠、可扩展和安全的方式来组织网络服务。通过CS模式,客户端可以方便地访问服务器提供的服务,而服务器则可以集中管理和控制服务的提供。

此处我们不在使用.hpp去封装他,直接写到main函数里:

首先,客户端必须要拿到具体的服务器IP和服务器端口号(实际中可能这两个东西是封装在客户端内部的)

发现此时还是要封装Die等宏,所以索性都引进到一个Commn头文件里。

之前的sockaddr_in中的结构体也做过类似思路的行为:

        

按照之前的逻辑,我们完成如下:

int main(int argc,char* argv[])
{   ENABLE_CONSOLE_LOG;if(argc!=3){LOG(LogLevel::ERROR)<<"Usage "<<argv[0]<<": 127.0.0.1 8080";Die(1);}//0.获取服务器的套接字信息std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);//1.创建套接字int sock_fd = socket(AF_INET,SOCK_DGRAM,0);if(sock_fd<0){LOG(LogLevel::FATAL)<<"socket fail";Die(2);}//2.填写server信息//因为后续要在sendto中直接给server通信sockaddr_in server_socket;server_socket.sin_family = AF_INET;server_socket.sin_port = ::htons(server_port);server_socket.sin_addr.s_addr = ::inet_addr(server_ip.c_str());// //3.进行bind,并且设置进内核// int ret = bind(sock_fd,CONV(&server_socket),sizeof(server_socket));while(true){std::cout<<"Please Enter"<<std::endl;std::string Message;std::getline(std::cin,Message);int n =  ::sendto(sock_fd,Message.c_str(),Message.length()-1,0,CONV(&server_socket),sizeof(server_socket));}}

注意,我们注释掉了bind那一步:

客户端不需要bind

        实际上,客户端并不需要绑定IP地址和端口。

注意,不需要不绑定表示:不需要程序员手动绑定,而是由OS自动绑定

理由如下:

比如你的手机,同时有淘宝客户端,LOL客户端,美团客户端,如果三个公司的工程师A\B\C描述自己的sockaddr的时候,都把同一个端口号描述成自己客户端的端口号,就会出现矛盾。

        如果客户端由程序员绑定,那么假设有两个公司上线的客户端使用的端口是一样的,就会出现一个软件先打开之后可以正常收到服务器发送的数据,但是另外一个软件的服务器就无法正确发送信息到对应的软件上即一个端口只能对应一个进程,但是一个进程可以有多个端口。

        那么,客户端难道不需要端口吗?并不是,如果客户端没有端口,那么服务器只能通过IP地址找到具体客户端设备,但是找不到对应的进程,既然如此,客户端的端口怎么确定?实际上这个端口由操作系统自行随机分配。所以,端口号会在第一次sendto之后自动绑定。

 那么服务器端又为什么需要程序员手动绑定端口号? 

       因为服务器端口号如果是随机的,而软件中请求服务器的端口号是固定的,那么一个软件可能在某一天可以正常收到服务器发送的数据,但是下一次因为服务器端口号是变化的,就无法正常收到信息。也就是说,端口号高概率会内置在客户端中,如果服务器一直变化,就很难通信。

综上所述,服务器端需要程序员手动绑定IP地址和端口号,而客户端不需要程序员手动绑定IP地址和端口号,由操作系统自行分配并绑定启动

         服务器端口号不仅不改变,还要尽量做到“众所周知”,方便大家来连接。

一般在公司中,一个准备上线的项目都需要去公司后台申请,申请到了对应的端口才能使用。

        所以在这段代码中,sock_fd描述的是发送方的动作,server_socket是希望接受发送动作的信息的地址。

         因此,两个东西本来含义就不匹配,在含以上肯定不能bind。结合上面的内容我们可以知道,这个sock_fd最终被OS自动绑定。

最终调试可运行代码效果:

 

 第一版本到此结束,源码在这里:

EchoServer_V1 · lsnmjp/code of cpp Linux 算法 - 码云 - 开源中国

现在能本地通信了,从此我们似乎以及不再需要之前的SYSTEM V体系了.......

准备联入网络。


3. demo代码优化

3.1 从sockaddr_in中取数据

作为服务器,我们希望在传输出型变量peer出去之后将peer利用起来。

所以我们学习使用接口:

                 

其中ntohs肯定可以用来获得port

其次,想要将IP转化为char*方便我们看,所以有:inet_ntoa

所以:

int n =  recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&peer_len);//此时peer已经获得了对应的数据
uint16_t client_port = ::ntohs(peer.sin_port); //网络字节序列转主机序列
std::string client_IP = ::inet_ntoa(peer.sin_addr);//转换到主机序列;转换成字符串

再在服务器echo的时候包装一下:

57817就是我们的客户端自动被OS绑定的端口号!


3.2 是否需要IP地址

修改服务器端的绑定网络:

刚刚一直使用的是127.0.0.1的本地网卡,现在试试手动输入一个——如果以机器的公网IP为例输入会发现:

之所以出现这个问题,是因为云服务器的公网IP地址是不允许用户自行绑定

解决这个问题之前,我们思考一下,启动一个服务器真的必须要绑定IP吗?

 

        一台服务器可能有多个IP地址,此时如果服务器固定IP地址,那么此时就会出现服务器只能接收传送到固定IP地址的信息,就算服务器有很多IP地址也只有一个IP地址可以使用,很明显这个效果并不符合UDP协议的特点,因为UDP协议是面向无连接的,既然都不需要连接,为什么还需要指定IP地址,所以启动服务器不需要指定IP地址。

        ​​​​​​​         

宏INADDR_ANY表示可以接受该机器所拥有的所有IP。注意,并不是通过INADDR_ANY去找机器,找机器的时候还是通过客户端所内置的IP地址。但是在找到之后,在服务器的视角,只要是属于自己的IP,都照单全收。

并且在构造函数和private下面可以不再需要IP变量。

        ​​​​​​​        

此时的状态是,server不需输入IP,但是client依然靠已知的IP去找server。

当下代码,已经可以在多个LInux上跑了,也就是可以让多个Linux机器之间互动。

甚至也可以在Linux和win下通过网络通信(了解):

win的内核层一模一样:

3.3 字节序列以及设置地址封装

为了便于之后的sockaddr_in地址和我们希望看到的string ip与uint16_t port更直观,我们进行以下封装:

#pragma once#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>namespace Inet_Addr
{class InetAddr{private:void Host2Net()//如果先拿到主机,我们希望转向网络{_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}//如果先拿到网络,我们希望将地址转向主机void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IPNet2Host(){_ip = }public:InetAddr(){}InetAddr(const struct sockaddr_in& net_addr):_net_addr(net_addr)//如果先拿到网络,我们希望将地址转向主机{PortNet2Host();IPNet2Host();}InetAddr(uint16_t port):_port(port)//如果先拿到主机,我们希望将地址转向网络{Host2Net();}private:struct sockaddr_in _net_addr; // 网络字节标准uint16_t _port;               // 主机标准std::string _ip;              // 主机标准};
}

此处的ip本可以用之前学习的inet_ntoa转换。

但其实,作为一个返回char*的C语言函数并不安全。指针会在inet_ntoa函数内部维护一段静态空间,在多线程情况,这个空间可能被覆盖。

3.3.1 线程安全接口

我们采用一种更加线程安全的inet_ntop方法:

  void IPNet2Host(){char buffer[64];_ip = ::inet_ntop(AF_INET,&_net_addr.sin_addr,buffer,sizeof(_net_addr));}

会把转换好的数据先放在buffer里,然后再赋值给_ip。

因为buffer是在线程栈是创造的,所以不会矛盾。


继续封装: 

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Commn.hpp"namespace Inet_Addr
{class InetAddr{private:void Host2Net()//如果先拿到主机,我们希望转向网络{_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}//如果先拿到网络,我们希望将地址转向主机void PortNet2Host(){_port = ::ntohs(_net_addr.sin_port);}void IPNet2Host(){char buffer[64];_ip = ::inet_ntop(AF_INET,&_net_addr.sin_addr,buffer,sizeof(_net_addr));}public:InetAddr(){}InetAddr(const struct sockaddr_in& net_addr):_net_addr(net_addr)//如果先拿到网络,我们希望将地址转向主机{PortNet2Host();IPNet2Host();}InetAddr(uint16_t port):_port(port)//如果先拿到主机,我们希望将地址转向网络{Host2Net();}uint16_t GetPort(){return _port;}std::string& GetIP(){return _ip;}struct sockaddr* NetAddr(){return CONV(&_net_addr);}socklen_t NetAddrLen(){return sizeof(_net_addr);}private:struct sockaddr_in _net_addr; // 网络字节标准uint16_t _port;               // 主机标准std::string _ip;              // 主机标准};
}

到客户端和服务器中去封装:

        

改动后的完整代码:

#ifndef UDP_SERVER__HPP
#define UDP_SERVER__HPP#include <iostream>
#include <memory>
#include <string>
#include <string.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Commn.hpp"using namespace LogModule;
using namespace Inet_Addr;// global
int gsockfd = -1;
const static std::string gdefaultIP = "127.0.0.1";
const static uint16_t gdefaultport = 8080;class UdpServer
{
public:// UdpServer( const std::string& IP = gdefaultIP,uint16_t port = gdefaultport)UdpServer(uint16_t port = gdefaultport): _sockfd(gsockfd), _inetaddr(port)//,_port(port)//,_IP(IP){}void InitServer(){// ENABLE_FILE_LOG;//日志文件使能ENABLE_CONSOLE_LOG;// 1.创建套接字_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){Die(1);LOG(LogLevel::FATAL) << "socket: " << strerror(errno);}// 创建成功,看看套接字LOG(LogLevel::INFO) << "socket success , socket fd is :" << _sockfd;// 2.填充网络信息并bind : 设置进了内核中//  struct sockaddr_in* in_addr_;//  in_addr_ = new sockaddr_in();// 2.1 填充in_addr的信息//  bzero(in_addr_, sizeof(sockaddr_in));//清理//  in_addr_->sin_family = AF_INET;//  in_addr_->sin_port = ::htons(_port);//端口号//  //in_addr_->sin_addr.s_addr = ::inet_addr(_IP.c_str());//1. string ip->4bytes 2. network order//  in_addr_->sin_addr.s_addr = INADDR_ANY;// bindint n = bind(_sockfd, CONV(_inetaddr.NetAddr()), _inetaddr.NetAddrLen());if (n < 0){LOG(LogLevel::ERROR) << "bind fail";exit(1);}LOG(LogLevel::INFO) << "bind success";}void Start(){_isrunning = true;while (true){char inbuffer[SIZE];sockaddr_in peer;socklen_t peer_len = sizeof(peer);int n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &peer_len);// // 此时peer已经获得了对应的数据// uint16_t client_port = ::ntohs(peer.sin_port);      // 网络字节序列转主机序列// std::string client_IP = ::inet_ntoa(peer.sin_addr); // 转换到主机序列;转换成字符串if (n > 0){InetAddr client(peer);inbuffer[n] = 0;LOG(LogLevel::DEBUG) << "Client says@ " << inbuffer;std::string echo = "#echo: ";std::string backinfo = client.GetIP() + " " + std::to_string(client.GetPort()) + echo;// std::string client_info = client_IP;// client_info += " : ";// client_info += std::to_string(client_port);// client_info += '  ';// echo += client_info;backinfo += inbuffer;int ret = sendto(_sockfd, backinfo.c_str(), backinfo.length(), 0, CONV(client.NetAddr()), client.NetAddrLen());// if(ret>0)// {//     LOG(LogLevel::DEBUG)<<"server sendto success";// }// else// {//     LOG(LogLevel::DEBUG)<<"server sendto fail";// }}}_isrunning = false;}~UdpServer(){if (_sockfd != gsockfd)close(_sockfd);}private:int _sockfd; // 访问套接字的文件描述符InetAddr _inetaddr;// uint16_t _port; //端口号// std::string _IP;//十进制点分IP地址bool _isrunning = false;
};#endif

全部代码:

EchoServer_V2 · lsnmjp/code of cpp Linux 算法 - 码云 - 开源中国

白框上面是xshell打开的client,白框下面是vscode打开的server端。

4. 下集预告 

下一集我们将利用上述思维和代码进行一些简单的业务,比如字典、聊天室等

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

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

相关文章

MySQL的索引事务

索引 是什么 类似于目录&#xff0c;提高查询的速度&#xff0c;但是本身会占用空间&#xff0c;增删数据的时候也需要维护索引。所以查询操作频繁的时候可以创建索引。如果非条件查询列&#xff0c;或经常做插入、修改操作&#xff0c;或磁盘空间不足时&#xff0c;不考虑创…

安卓9.0系统修改定制化____第三方美化 bug修复 移植相关 辅助工具 常识篇 八

在修改rom中。有时候不可避免的需要对系统进行美化以及一些第三方系统的bug修复。在操作前需要了解系统的一些基本常识。例如同平台移植 跨平台移植以及内核移植 apk反编译等等相关的知识。今天解析的这款工具虽然不是直接面向安卓9.0.但对于了解以上的一些必备常识还是不错的 …

云服务器与物理服务器对比:选择最适合的业务服务器解决方案

更多云服务器知识&#xff0c;尽在hostol.com 在现代 IT 基础设施中&#xff0c;云服务器与物理服务器是两种常见的服务器解决方案。随着云计算技术的迅猛发展&#xff0c;越来越多的企业开始转向云服务器&#xff0c;但也有一些企业仍然坚持使用物理服务器&#xff0c;尤其是…

【redis使用场景——缓存——双写一致性】

redis使用场景——缓存——双写一致性 双写一致性问题的本质与场景典型不一致场景分析​​并发写操作导致的不一致​​​​读写交叉导致的不一致​​​​主从同步延迟导致的不一致​​ 解决延迟双删策略&#xff08;推荐&#xff09;优点​​&#xff1a;​​缺点​​&#xff…

【ArcGIS】在线影像底图调用

【ArcGIS】在线影像底图调用 一、 历史影像的调用二、ArcGIS online底图调用三、结语 一、 历史影像的调用 ESRI官方推出了World Imagery Wayback是一个提供全球范围内历史影像的在线服务。 官网地址&#xff1a;https://livingatlas.arcgis.com/wayback/ 操作步骤&#xff1…

密度估计:从零星足迹重建整体画像

想象你是一位侦探&#xff0c;案发现场只留下几个零散的脚印。**如何通过这些碎片&#xff0c;推断嫌疑人的身高体重&#xff1f;甚至预测他下一步的藏身之处&#xff1f;** 这种从局部反推整体的能力&#xff0c;正是**密度估计&#xff08;Density Estimation&#xff09;** …

B004基于STM32F401单片机简易交通灯实训数码管显示设计仿真资料

视频演示地址:https://www.bilibili.com/video/BV1GvNDzFEd9/ 运行环境 仿真软件:proteus8.17(切记别的版本不能运行) 编程软件:MDK525 STM32 cubmx版本:6.11.1(切记别的版本不能运行) 原理图画图软件:AD10 功能说明&#xff1a; 以STM32F401CB单片机为核心简易交通灯功能如下。…

没掌握的知识点记录

1、微内核的主要优点在于结构清晰、内核代码量少&#xff0c;安全性和可靠性高、可移植性强、可伸缩性、可扩展性高&#xff1b;其缺点是难以进行良好的整体优化、进程间互相通信的开销大、内核功能代码不能被直接调用而带来服务的效率低。 2、题目&#xff1a; 分页内存管理…

linux 远程终端执行qt应用显示到接入的物理显示器上

在显示器打开终端执行&#xff1a; xhost local: 在远程终端执行&#xff1a; export DISPLAY:0然后在终端执行qt应用就可以。 xhost local: 功能&#xff1a;允许本地用户&#xff08;local:&#xff09;访问 X 服务器&#xff08;X11 图形系统&#xff09;。 原理&#xf…

【AI驱动网络】

一、AI 驱动网络 1.1 什么是网络 1.1.1、网络的定义 ​网络是由若干节点​(如计算机、服务器、移动设备等)和连接这些节点的链路​(有线或无线传输介质)构成的系统,用于实现地理位置分散的独立设备之间的信息交换、资源共享与协同工作。在计算机领域,网络是信息传输、…

Python期末速成

一.基础内容 赋值语句&#xff1a; a 1 b "mayday" 标识符规则&#xff1a; 1.字母&#xff0c;数字&#xff0c;下划线&#xff0c;汉字组成。但数字不能开头 2.不能是保留字 3.特殊符号不行&#xff0c;*&#xffe5;^等 注释是在语句前面加&#xff03; …

【时时三省】(C语言基础)指针变量例子

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 存放地址的变量是指针变量&#xff0c;它用来指向另一个对象&#xff08;如变量、数组、函数等&#xff09;。 那么&#xff0c;怎样定义和使用指针变量呢? 先分析一个例子。 例题 通过…

MATLAB代码演示,TDOA定位的优化算法,提升Z轴的定位精度|复现《基于最小二乘法的室内三维定位算法研究》

本文复现文章: 王桂杰,焦良葆,曹雪虹.基于最小二乘法的室内三维定位算法研究[J].计算机技术与发展,2020,30(04):69-73.按照文章的核心算法,复现了TDOA下的最小二乘在三维环境中的改进定位方法,方法可以明显提升Z轴的定位精度 文章目录 概述运行结果展示matlab代码完整代码概…

React useState 原理

Fiber架构 React16 之后 提升显示性能 电脑屏幕参数刷新率 表示1s刷新次数 页面渲染 和 JS代码执行 共享一个线程 互斥 保持上一帧图像表现&#xff1a;卡顿 reconcilier改为 stack 和 fiber Fiber数据结构 执行单元 浏览器优先执行用户响应相关或者界面渲染相关事件&#…

【Datawhale组队学习202506】零基础学爬虫 01 初始爬虫

系列文章目录 01 初始爬虫 02 数据解析与提取 文章目录 系列文章目录前言1 爬虫和Python2 爬虫的矛盾2.1 爬虫与反爬2.2 robots核心字段重要规则说明非标准扩展指令协议生效条件局限性验证工具 2.3 一个爬虫demo 3 Web请求与HTTP协议3.1 一个web请求的全过程3.2 判断页面源代码…

前端面试十之vuex

Vuex 是一个专为 Vue.js 应用程序设计的状态管理模式和库&#xff0c;它集中管理应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。以下是关于 Vuex 的详细介绍&#xff1a; 1. 核心概念 State&#xff08;状态&#xff09; 它是 Vuex 中存…

Django中为api自定义一些装饰器:如参数校验等

在Django中使用了rest_framework时&#xff0c;一般我们会定义ModelSerializer来校验request.data中参数是否存在和参数类型。 但当我们只是想简单校验一些api的url上是否存在某些参数时&#xff0c;该怎么办&#xff1f;当然我们也可以通过定义Serializer来实现&#xff0c;但…

uni-app项目实战笔记21--uniapp缓存的写入和读取

一、缓存的写入 uni.setStorageSync("storageClassList",classifyList.value) 二、缓存的读取&#xff0c;如果缓存不存在&#xff0c;则返回空数组 const storageClassList uni.getStorageSync("storageClassList") || []; 三、对读取到的数据进行转…

Zama密码分析资助计划

1. 引言 2025年5月&#xff0c;Zama团队正式启动了 Zama 密码分析资助计划&#xff08;Cryptanalysis Grant Program&#xff09;&#xff0c;以支持那些致力于“破解”系统的研究人员&#xff1a; 无论是通过密码分析、侧信道攻击、故障注入&#xff0c;还是其他创新性方法。…

【数据结构与算法】数据结构初阶:详解顺序表和链表(一)

&#x1f525;个人主页&#xff1a;艾莉丝努力练剑 ❄专栏传送门&#xff1a;《C语言》、《数据结构与算法》 &#x1f349;学习方向&#xff1a;C/C方向 ⭐️人生格言&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;为万世开太平 前言&am…