【Hello Network】网络编程套接字(三)

news2024/12/22 22:16:29

作者:@小萌新
专栏:@网络
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:简单介绍下各种类型的Tcp协议

各种类型Tcp服务器

  • 多进程版的TCP网络程序
    • 捕捉SIGCHLD信号
    • 让孙子进程执行任务
  • 多线程TCP网络程序
  • 线程池版多线程TCP网络程序

我们在前面的网络编程套接字(二)中写出了一个单执行流的服务器

我们再来回顾下它的运行

在这里插入图片描述
我们首先启动服务器 之后启动客户端1 最后启动客户端2

我们发现启动客户端1之后向服务器发送数据服务器很快的就回显了一个数据并且打印了得到一个新连接

可是在客户端2连接的时候却没有发生任何情况

在这里插入图片描述

当我们的客户端1退出的时候 服务器接受到了客户端2的连接并且回显了数据

单执行流服务器

这是因为我们的服务器是单执行流的 所以在同一时间只能有一个客户端接受服务

当服务端调用accept函数获取到连接后就给该客户端提供服务 但在服务端提供服务期间可能会有其他客户端发起连接请求 但由于当前服务器是单执行流的 只能服务完当前客户端后才能继续服务下一个客户端

客户端为什么会显示连接成功

服务器是处于监听状态的 在我们的客户端2发送连接请求的时候实际上已经被监听到了 只不过服务端没有调用accept函数将该连接获取上来

实际在底层会为我们维护一个连接队列 服务端没有accept的新连接就会放到这个连接队列当中 而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的 因此服务端虽然没有获取第二个客户端发来的连接请求 但是在第二个客户端那里显示是连接成功的

如何解决?

单执行流的服务器一次只能给一个客户端提供服务 此时服务器的资源并没有得到充分利用 因此服务器一般是不会写成单执行流的 要解决这个问题就需要将服务器改为多执行流的 此时就要引入多进程或多线程

多进程版的TCP网络程序

我们将之前的单执行流服务器改为多进程服务器

当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务 而是当前执行流调用fork函数创建子进程 然后让子进程为父进程获取到的连接提供服务

由于父子进程是两个不同的执行流 当父进程调用fork创建出子进程后 父进程就可以继续从监听套接字当中获取新连接 而不用关心获取上来的连接是否服务完毕

子进程继承父进程的文件描述符表

需要注意的是 文件描述符表是隶属于一个进程的 子进程创建后会继承父进程的文件描述符表

比如父进程打开了一个文件 该文件对应的文件描述符是3 此时父进程创建的子进程的3号文件描述符也会指向这个打开的文件 而如果子进程再创建一个子进程 那么子进程创建的子进程的3号文件描述符也同样会指向这个打开的文件

在这里插入图片描述

但是当父进程创建出子进程之后 父子进程就会保持独立性了 此时父进程文件描述符表的变化不会影响子进程的文件描述符表

在我们之前学习的匿名管道通信时 我们就是使用的这个原理

父进程首先使用pipe函数得到两个文件描述符 一个是文件读端一个是文件的写端 此时父进程创建的子进程会继承这两个文件描述符

之后父子进程一个关闭管道的读端 另一个关闭管道的写端 这时父子进程文件描述符表的变化是不会相互影响的 此后父子进程就可以通过这个管道进行单向通信了

对于套接字文件也是一样的 父进程创建的子进程也会继承父进程的套接字文件 此时子进程就能够对特定的套接字文件进行读写操作 进而完成对对应客户端的服务

等待子进程问题

当父进程创建出子进程后 父进程是需要等待子进程退出的 否则子进程会变成僵尸进程 进而造成内存泄漏

因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待

此时我们就有两种等待方式 阻塞式等待和非阻塞式等待:

  • 如果服务端采用阻塞的方式等待子进程 那么服务端还是需要等待服务完当前客户端 才能继续获取下一个连接请求此时服务端仍然是以一种串行的方式为客户端提供服务
  • 如果服务端采用非阻塞的方式等待子进程 虽然在子进程为客户端提供服务期间服务端可以继续获取新连接 但此时服务端就需要将所有子进程的PID保存下来 并且需要不断花费时间检测子进程是否退出

总之 服务端要等待子进程退出 无论采用阻塞式等待还是非阻塞式等待 都不尽人意 此时我们可以考虑让服务端不等待子进程退出

不等待子进程退出的方式

让父进程不等待子进程退出 常见的方式有两种:

  • 捕捉SIGCHLD信号 将其处理动作设置为忽略
  • 让父进程创建子进程 子进程再创建孙子进程 最后让孙子进程为客户端提供服务

捕捉SIGCHLD信号

实际当子进程退出时会给父进程发送SIGCHLD信号 如果父进程将SIGCHLD信号进行捕捉 并将该信号的处理动作设置为忽略 此时父进程就只需专心处理自己的工作 不必关心子进程了

下面是我们的处理代码 其中比较核心的代码是这一行

signal(SIGCHLD, SIG_IGN);
class TcpServer
{
public:
	void Start()
	{
		signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				//处理请求
				Service(sock, client_ip, client_port);
				exit(0); //子进程提供完服务退出
			}
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

下面是在此运行的结果

在这里插入图片描述

我们可以发现 加上这几行代码之后我们就可以让服务器服务多个客户端了

让孙子进程执行任务

我们也可以让服务端创建出来的子进程再次进行fork 让孙子进程为客户端提供服务 此时我们就不用等待孙子进程退出了

命名:

  • 爷爷进程:在服务端调用accept函数获取客户端连接请求的进程
  • 爸爸进程:由爷爷进程调用fork函数创建出来的进程
  • 孙子进程:由爸爸进程调用fork函数创建出来的进程 该进程调用Service函数为客户端提供服务

我们让爸爸进程创建完孙子进程后立刻退出 此时服务进程(爷爷进程)调用wait/waitpid函数等待爸爸进程就能立刻等待成功 此后服务进程就能继续调用accept函数获取其他客户端的连接请求

不需要等待孙子进程退出

这里主要是利用了孤儿进程的原理 当孙子进程的父进程死亡后它就会被1号进程也就是init进程领养 当孙子进程运行完毕之后它的资源会由1号进程进行回收 我们也就不需要担心僵尸进程的问题了

关闭对应的文件描述符

服务进程(爷爷进程)调用accept函数获取到新连接后 会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程 而爸爸进程又会调用fork函数创建出孙子进程 然后再将文件描述符表继承给孙子进程。

而父子进程创建后 它们各自的文件描述符表是独立的 不会相互影响

因此服务进程在调用fork函数后 服务进程就不需要再关心刚才从accept函数获取到的文件描述符了 此时服务进程就可以调用close函数将该文件描述符进行关闭

同样的 对于爸爸进程和孙子进程来说 它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的 因此爸爸进程可以将监听套接字关掉

关闭文件描述符的必要性:

  • 对于服务进程来说 当它调用fork函数后就必须将从accept函数获取的文件描述符关掉 因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字) 如果服务进程不及时关掉不用的文件描述符 最终服务进程中可用的文件描述符就会越来越少
  • 而对于爸爸进程和孙子进程来说 还是建议关闭从服务进程继承下来的监听套接字 实际就算它们不关闭监听套接字 最终也只会导致这一个文件描述符泄漏 但一般还是建议关上 因为孙子进程在提供服务时可能会对监听套接字进行某种误操作 此时就会对监听套接字当中的数据造成影响
class TcpServer
{
public:
	void Start()
	{
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '\0', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				close(_listen_sock); //child关闭监听套接字
				if (fork() > 0){
					exit(0); //爸爸进程直接退出
				}
				//处理请求
				Service(sock, client_ip, client_port); //孙子进程提供服务
				exit(0); //孙子进程提供完服务退出
			}
			close(sock); //father关闭为连接提供服务的套接字
			waitpid(id, nullptr, 0); //等待爸爸进程(会立刻等待成功)
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

运行结果如下

在这里插入图片描述
我们可以发现当前服务器可以支持多个客户端访问并且得到的文件描述符都是4

多线程TCP网络程序

创建进程的成本是很高的,创建进程时需要创建该进程对应的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等数据结构。而创建线程的成本比创建进程的成本会小得多,因为线程本质是在进程地址空间内运行,创建出来的线程会共享该进程的大部分资源,因此在实现多执行流的服务器时最好采用多线程进行实现

当服务进程调用accept函数获取到一个新连接后 就可以直接创建一个线程 让该线程为对应客户端提供服务

当然 主线程(服务进程)创建出新线程后 也是需要等待新线程退出的 否则也会造成类似于僵尸进程这样的问题 但对于线程来说 如果不想让主线程等待新线程退出 可以让创建出来的新线程调用pthread_detach函数进行线程分离 当这个线程退出时系统会自动回收该线程所对应的资源 此时主线程(服务进程)就可以继续调用accept函数获取新连接 而让新线程去服务对应的客户端

各个线程共享同一张文件描述符表

文件描述符表维护的是进程与文件之间的对应关系 因此一个进程对应一张文件描述符表

而主线程创建出来的新线程依旧属于这个进程 因此创建线程时并不会为该线程创建独立的文件描述符表 所有的线程看到的都是同一张文件描述符表

在这里插入图片描述

因此当服务进程(主线程)调用accept函数获取到一个文件描述符后 其他创建的新线程是能够直接访问这个文件描述符的

需要注意的是 虽然新线程能够直接访问主线程accept上来的文件描述符 但此时新线程并不知道它所服务的客户端对应的是哪一个文件描述符

因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符的值 也就是告诉每个新线程在服务客户端时 应该对哪一个套接字进行操作

文件描述符关闭的问题

由于此时所有线程看到的都是同一张文件描述符表 因此当某个线程要对这张文件描述符表做某种操作时 不仅要考虑当前线程 还要考虑其他线程

  • 对于主线程accept上来的文件描述符 主线程不能对其进行关闭操作 该文件描述符的关闭操作应该由新线程来执行 因为是新线程为客户端提供服务的 只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭
  • 对于监听套接字 虽然创建出来的新线程不必关心监听套接字 但新线程不能将监听套接字对应的文件描述符关闭 否则主线程就无法从监听套接字当中获取新连接了

Service函数定义为静态成员函数

由于调用pthread_create函数创建线程时 新线程的执行例程是一个参数为void* 返回值为void*的函数 如果我们要将这个执行例程定义到类内 就需要将其定义为静态成员函数 否则这个执行例程的第一个参数是隐藏的this指针

在线程的执行例程当中会调用Service函数 由于执行例程是静态成员函数 静态成员函数无法调用非静态成员函数 因此我们需要将Service函数定义为静态成员函数 恰好Service函数内部进行的操作都是与类无关的 因此我们直接在Service函数前面加上一个static即可

Rontine函数

  static void* Rontine(void* arg)
  {
    pthread_detach(pthread_self());
    int* p = (int*)arg;

    int sock = *p;
    Service(sock);
    return nullptr;
  }

Start函数

  void Start()
  {
    while(true)
    {
      // accept 
      struct sockaddr_in peer; 
      memset(&peer , '\0' , sizeof(peer));
      socklen_t len = sizeof(peer);

      int sock = accept(_sockfd , (struct sockaddr*)&peer , &len);
      if (sock < 0)
      {
        cout << "accept error" << endl; 
        continue;
      }

      int* p = &sock; 
      pthread_t tid;
      pthread_create(&tid , nullptr , Rontine , (void*)p);
    }
  }

运行结果如下
在这里插入图片描述

线程池版多线程TCP网络程序

当前多线程版的服务器存在的问题:

  • 每当有新连接到来时 服务端的主线程都会重新为该客户端创建为其提供服务的新线程 而当服务结束后又会将该新线程销毁 这样做不仅麻烦 而且效率低下 每当连接到来的时候服务端才创建对应提供服务的线程
  • 如果有大量的客户端连接请求 此时服务端要为每一个客户端创建对应的服务线程 计算机当中的线程越多 CPU的压力就越大 因为CPU要不断在这些线程之间来回切换 此时CPU在调度线程的时候 线程和线程之间切换的成本就会变得很高
  • 一旦线程太多 每一个线程再次被调度的周期就变长了 而线程是为客户端提供服务的 线程被调度的周期变长 客户端也迟迟得不到应答

解决思路

  • 可以在服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程。
  • 当某个线程为客户端提供完服务后,不要让该线程退出,而是让该线程继续为下一个客户端提供服务,如果当前没有客户端连接请求,则可以让该线程先进入休眠状态,当有客户端连接到来时再将该线程唤醒。
  • 服务端创建的这一批线程的数量不能太多,此时CPU的压力也就不会太大。此外,如果有客户端连接到来,但此时这一批线程都在给其他客户端提供服务,这时服务端不应该再创建线程,而应该让这个新来的连接请求在全连接队列进行排队,等服务端这一批线程中有空闲线程后,再将该连接请求获取上来并为其提供服务。

我们可以发现 我们前面做的线程池可以完美解决上面的问题

线程池

服务类新增线程池成员

服务类新增线程池成员

  • 当实例化服务器对象时,先将这个线程池指针先初始化为空。
  • 当服务器初始化完毕后,再实际构造这个线程池对象,在构造线程池对象时可以指定线程池当中线程的个数,也可以不指定,此时默认线程的个数为5。
  • 在启动服务器之前对线程池进行初始化,此时就会将线程池当中的若干线程创建出来,而这些线程创建出来后就会不断检测任务队列,从任务队列当中拿出任务进行处理。

现在当服务进程调用accept函数获取到一个连接请求后,就会根据该客户端的套接字、IP地址以及端口号构建出一个任务,然后调用线程池提供的Push接口将该任务塞入任务队列

这实际也是一个生产者消费者模型,其中服务进程就作为了任务的生产者,而后端线程池当中的若干线程就不断从任务队列当中获取任务进行处理,它们承担的就是消费者的角色,其中生产者和消费者的交易场所就是线程池当中的任务队列。

    void Start()                                                           
    {                                                                      
      _tp->ThreadPoolInit();                                               
      while(true)                                                          
      {                                                                    
        // accept                                                          
        struct sockaddr_in peer;                                           
        memset(&peer , '\0' , sizeof(peer));                               
        socklen_t len = sizeof(peer);                                      
                                                                           
        int sock = accept(_sockfd , (struct sockaddr*)&peer , &len);       
        if (sock < 0)                                                      
        {                                                                  
          cout << "accept error" << endl;                                  
          continue;                                                        
        }                                                                                                                                           
                                                                           
E>      Task task(port);                                                   
        _tp->Push(task);    
      }                                                                   
    }  

设计任务类

现在我们要做的就是设计一个任务类,该任务类当中需要包含客户端对应的套接字、IP地址、端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个。

此外,任务类当中需要包含一个Run方法,当线程池中的线程拿到任务后就会直接调用这个Run方法对该任务进行处理,而实际处理这个任务的方法就是服务类当中的Service函数,服务端就是通过调用Service函数为客户端提供服务的。

我们可以直接拿出服务类当中的Service函数,将其放到任务类当中作为任务类当中的Run方法,但这实际不利于软件分层。我们可以给任务类新增一个仿函数成员,当执行任务类当中的Run方法处理任务时就可以以回调的方式处理该任务。

Handler类

  class Handler
  {
    Handler() = default;
    void operator()(int sock)
    {
        cout << "get a new linl :  " << sock  << endl;
        char buff[1024];                                                                                                                                                   
        while(true)     
        {                          
          ssize_t size = read(sock , buff , sizeof(buff) - 1);
          if (size > 0)               
          {
            buff[size] = 0; // '\0'
            cout << buff << endl;
            write(sock , buff , size);   
          }
          else if (size == 0)
          {       
            cout << "read close" << endl;
            break;                                                                                                                                  
          }
          else
          {         
            cout << "unknown error" << endl;      
          }
       }
        close(sock);
        cout << "Service end sock closed" << endl;
    }
  };

Task类

#pragma once     
#include "sever.cc"    
#include <iostream>    
using namespace std;    
class Task    
{    
  private:    
    int _sock;    
    Handler _handler;    
  public:    
    Task(int sock)    
      :_sock(sock)                                                                                                                                  
    {}    
    Task() = default;   
    void run()    
    {    
      _handler(_sock);    
    }    
};  

这样子我们线程池版本的TCP网络程序就基本完成了

下面是运行结果

在这里插入图片描述

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

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

相关文章

Elasticsearch学习,请先看这篇!

目录 一、初始elasticsearch 1、概述 简介 发展 2、倒排索引 3、基本概念 文档 索引 Mysql和es的区别 4、分词器 初始分词器 Ik分词器-扩展词库 二、索引库操作 1、mapper属性 2、创建索引库 3、查询、删除索引库 三、文档操作 1、新增文档 2、查询、删除文档…

计算机网络科普

文章目录 1、集线器2、CSMA/CD协议3、交换机3.1 交换机的桥接 4、 路由器5、 路由表6、IP地址7、MAC地址8、ARP协议9、关于网络层次模型10、路由器 1、集线器 计算机之间的相互通信&#xff0c;你会怎么设计&#xff1f; 如果是两台计算机&#xff0c;之间拉一条线&#xff0c;…

ClickHouse性能优化

目录 1 Explain查看执行计划优化sql1.1 基本语法1.2 案例实操1.2.1 查看 PLAIN1.2.2 AST 语法树1.2.3 SYNTAX 语法优化1.2.4 查看 PIPELINE 2 ClickHouse建表优化2.1 数据类型2.1.1 时间字段的类型2.1.2 空值存储类型 2.2 分区和索引2.3 表参数2.4 写入和删除优化2.5 常见配置2…

分享一些提升效率的小工具

1、 IObit Uninstaller IObit Uninstaller是一款简单专业的卸载工具&#xff0c;可以帮我们卸载电脑中顽固难卸的软件和浏览器插件&#xff0c;支持强制一键卸载和文件粉碎功能。 除了卸载软件&#xff0c;它还可以自动帮我们检测软件安装、检测软件更新、查看工具栏和插件。 …

IDEA22.3.3的三个常用经常遇到的配置问题

1、期待效果&#xff1a;【打开iDEA的时候&#xff0c;让开发者选择需要打开的项目】 设置如下 2、期待效果&#xff1a;配置默认的Maven&#xff0c;避免每次新建项目后&#xff0c;都需要去修改Maven配置 同理&#xff0c;修改默认的java版本和自己本地java环境一致 3、新建…

数据库SQL语句优化技巧

当今世界&#xff0c;数据量不断增长&#xff0c;数据库的使用变得越来越普遍。虽然数据库提供了很多强大的功能&#xff0c;但是它们也需要被优化以确保它们的性能得到最大化。在本篇博客中&#xff0c;我们将探讨SQL语句优化的几种技巧&#xff0c;这些技巧可以帮助您提高数据…

零、网络基础概述(TCP/IP模型、端口、网关、DNS、ARP、IP编址与子网划分、UDP、VRP)

文章目录 前言一、网络基础1、TCP/IP模型2、端口的作用&#xff1a;3、MAC 地址4、网关&#xff08;gateway&#xff09;5、域名解析服务&#xff08;DNS&#xff09;6、TCP端口、UDP端口区别&#xff1a;7、交换机与路由器 二、ARP 理论1、定义2、查看ARP缓存3、ARP 报文种类&…

Linux基础——远程访问及控制(SSH)

Linux基础——远程访问及控制 一、OpenSSH服务器二、sshd_config配置文件三、SSH服务端1.查询版本—— ssh -V2.SSH远程登录3.监听端口修改4.设置黑白名单5.远程复制——scp6.安全性传输——sftp 四、SSH服务的验证1.SSH服务的两种验证方式密码验证密钥验证 3.公钥与私钥的关系…

ORA-04021:等待锁定对象时发生超时

现场人员反馈问题&#xff0c;drop表报错&#xff0c;如下图 是个rac环境&#xff0c;处理过程 1、2个节点上查看锁表&#xff0c;没任何输出 SYSorcl2> select name from v$db_object_cache where ownerUSR_DATAI and type in(PROCEDURE,FUNCTION) and locks > 0 and …

软件版本号

版本号 上图是在MVN仓库中随便找的一个依赖的历 史版本 我们可以发现版本号一般是由 数字英文 组成 数字 一般大家都会看到1.x或者1.xx.xxx.Beta这种版本号&#xff0c;前面是数字 以 1.xx.xxx 为例 1是major号&#xff0c;一般重大更新会更新major号.xx或者.xx.xxx称为min…

arduino学习笔记1

一.hello word实验 1.基础结构 void setup() {// put your setup code here, to run once://设置初始状态&#xff0c;比如引脚、波特率等 }void loop() {// put your main code here, to run repeatedly://相当于main函数&#xff0c;但一直循环 }2.Serial&#xff08;串行通…

像素是什么

像素分为设备像素和设备无关像素。 下面说说来龙去脉。 一、显示器 显示图像的电子设备。 &#xff08;一&#xff09;显示器种类 1.LCD LCD&#xff08;Liquid crystal display&#xff09;&#xff0c;是液体晶体显示&#xff0c;也就是液晶显示器&#xff0c;LCD具有功耗低…

谷歌 Google Cloud 安装 NodeJS服务环境

目录 1. 安装 wget2. 安装 Node2.1 下载安装包2.2 安装包解压2.3 3 安装全局包并创建软链接 3. 安装 git 创建实例略过&#xff0c;点击 SSH 按钮&#xff0c; 在浏览器中打开SSH客户端 注&#xff1a; 本文基于 CentOS 9服务器操作系统 为了方便后面工具插件的顺利安装&a…

用PHP实现经典的4种排序算法

文章目录 一、前言二、4种排序算法2.1 快速排序2.2 插入排序2.3 选择排序2.4 冒泡排序 总结 一、前言 排序算法是一种将一组无序的数据元素按照某个规则(大小、字母序等)排列成有序的序列的算法。常见的排序算法包括冒泡排序、选择排序、插入排序、快速排序、归并排序等。 1.冒…

Python批量梯度下降法的举例

梯度下降法 梯度下降法是一种常用的优化算法&#xff0c;用于求解目标函数的最小值。其基本思想是&#xff0c;通过不断地朝着函数梯度下降的方向更新参数&#xff0c;直到找到函数的最小值。 具体来说&#xff0c;假设我们有一个可导的目标函数 f ( x ) f(x) f(x)&#xff…

项目五:使用路由器构建园区网

使用路由器构建园区网 1、新建拓扑2、配置交换机与主机3、配置路由交换机并进行通信4、通信测试5、配置路由器并进行通信测试1、配置路由器R-12、配置路由器R-2、R-33、通信测试 1、新建拓扑 依次添加四台主机&#xff0c;两台交换机&#xff0c;型号为S3700。两台路由交换机&…

体制内干部职务职级及领导干部排序对应关系大全

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; 一、公务员级别对应关系 &#xff08;一&#xff09;综合管理公务员职务与职级 1、职务分为10级&#xff0c;包括&#xff1a;正国职、副国职、正部职、副部职、正…

【WSN定位】基于加权双曲线的Dvhop定位算法【Matlab代码#16】

文章目录 1. 原始Dvhop定位2. 基于双曲线的Dvhop定位3. 对原始模型加权4. 部分代码5. 结果展示6. 资源获取7. 参考文献 1. 原始Dvhop定位 可参考Dvhop定位算法 2. 基于双曲线的Dvhop定位 双曲线定位算法是一种通过将待定位节点定位在以锚节点为焦点、两锚节点之间距离为焦距…

字符集与字符编码(ASCII、GBK、UNICODE)

1 常见编码 1.1 单字节编码&#xff1a;ASCII ASCII使用1个字节&#xff08;8个bit&#xff09;来记录一组常用字符&#xff0c;见下表&#xff1a; 例如其中字母a的二进制位&#xff1a;1100 001 97&#xff0c;那么a在计算机中就可以用1100001来保存。 注意上表中其实只…

【02-Java Web先导课】-Tomcat服务器的下载与安装

文章目录 前言一、Tomcat服务器&#xff08;apache-tomcat-8.5.28&#xff09;的 下载1、下载地址 二、Tomcat服务器的安装1、Tomcat目录结构2、Tomcat的启动与停止4、Tomcat启动成功后的测试 免责声明&#xff1a; 前言 Tomcat主要实现了Java EE中的Servlet、JSP规范&#xf…