当前位置: 首页 > news >正文

网络编程套接字(二)

目录

  • 简单的TCP网络程序
    • 服务端创建套接字
    • 服务端绑定
    • 服务端监听
    • 服务端获取连接
    • 服务端处理请求
    • 单执行流服务器的弊端
  • 多进程版TCP网络程序
    • 捕捉SIGCHLD信号
    • 让孙子进程提供服务
    • 多线程版的TCP网络程序
      • 客户端创建套接字
      • 客户端链接服务器
      • 客户端发起请求
    • 线程池版的TCP网络程序

简单的TCP网络程序

我们将TCP服务器封装成一个类:

class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), listensock(-1){}~TcpServer(){}private:uint16_t _port;std::string _ip;int listensock;
};

服务端创建套接字

首先我们要做的就是初始化服务器,而初始化服务器最先做的就是要创建套接字,TCP服务器在调用socket函数创建套接字时,参数设置如下:

  • 协议家族选择AF_INET,因为我们要进行的是网络通信。
  • 创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
  • 协议类型默认设置为0即可。

如果创建套接字后获得的文件描述符是小于0的,说明套接字创建失败,此时也就没必要进行后续操作了,直接终止程序即可。

void Serverinit()
{// 1.创建socketlistensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error:%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "create socket sucess:%d", listensock);
}

服务端绑定

套接字创建完毕后我们实际只是在系统层面上打开了一个文件,该文件还没有与网络关联起来,因此创建完套接字后我们还需要调用bind函数进行绑定操作。

TCP服务端绑定与UDP基本一致:

void Serverinit()
{// 1.创建socketlistensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error:%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "create socket sucess:%d", listensock);// 2. bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bond error:%d:%s", errno, strerror(errno));exit(2);}
}

服务端监听

UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态。

listen函数

设置套接字为监听状态的函数叫做listen,该函数的函数原型如下:

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符。
  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或20即可。

返回值说明:

  • 监听成功返回0,监听失败返回-1,同时错误码会被设置。

服务器监听

TCP服务器在创建完套接字和绑定后,需要再进一步将套接字设置为监听状态,监听是否有新的连接到来。如果监听失败也没必要进行后续操作了,因为监听失败也就意味着TCP服务器无法接收客户端发来的连接请求,因此监听失败我们直接终止程序即可。

void Serverinit()
{// 1.创建socketlistensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error:%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "create socket sucess:%d", listensock);// 2. bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bond error:%d:%s", errno, strerror(errno));exit(2);}// 3. 建立连接if (listen(listensock, gbacklog) < 0){logMessage(FATAL, "listen error:%d:%s", errno, strerror(errno));exit(3);}logMessage(NORMAL, "init server sucess");
}

服务端获取连接

TCP服务器初始化后就可以开始运行了,但TCP服务器在与客户端进行网络通信之前,服务器需要先获取到客户端的连接请求。

accept函数

获取连接的函数叫做accept,该函数的函数原型如下:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明:

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

accept函数返回值

调用accept函数过程中,我们是从监听套接字listensock中获取连接的,此时就会返回接收到套接字对应的文件描述符。accept函数中的监听套接字是为了让我们不断来获取连接的,而它返回的套接字才是真正为我们为您提供服务的。

    void start(){while (true){// sleep(1);// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}}}

服务端处理请求

此时服务端获取连接已经成功了,此时就需要对获取的连接进行处理,我们的客户端所接收的并不是监听套接字,而是accept函数返回的套接字,因为监听套接字一个连接完成就会继续连接下一个。

我们在这儿实现一个简单的回声服务器,服务端将客户端发出的数据进行打印,并将数据重新发回客户端,客户端拿到此数据以后在进行回显,这样就完成了服务端与客户端之间的通信。

read函数

TCP服务器读取数据的函数叫做read,该函数的函在这里插入代码片数原型如下:

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误。

注意

read返回值为0表示对端连接关闭,这就和我们学习管道通信一样:

  1. 写端不写,读端一直在读,此时读端就会就会被挂起;
  2. 读端不读,写端一直写,此时写端写满数据后就会被挂起;
  3. 写端写完数据后就关闭,读端就会将管道数据读到0后挂起;
  4. 读端进程直接关闭,此时写端进程就会被操作系统杀死,因为读端不会进行读取。

此时的客户端就对应写端,如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,因此如果服务端调用read函数后得到的返回值为0,此时服务端就不必再为该客户端提供服务了。

write函数

TCP服务器写入数据的函数叫做write,该函数的函数原型如下:

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

当服务端调用read函数收到客户端的数据后,就可以再调用write函数将该数据再响应给客户端。

服务端读取数据是从accept函数返回的套接字当中读取的,写入数据也是向accept函数返回的套接字当中写入的,所以为客户端提供的套接字是既可以读取数据,也可以写入数据的,这就是TCP全双工的体现。

在此我们还需注意的是从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完客户端后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{char buffer[1024];while (true){ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0) // 将发过来的数据当做字符串{buffer[s] = 0;std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;}else if (s == 0){logMessage(NORMAL, "%s:%d shutdown, me too!!!", clientip.c_str(), clientport);break;}else{logMessage(errno, "read socket error:%d %s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}close(sock);
}
void start()
{while (true){// sleep(1);// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = htons(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, sereversock:%d | %s | %d", serversock, client_ip.c_str(), client_port);// 开始通信//(1) 单进程循环,一次只处理一个客户端,处理完成以后在处理下一个service(serversock, client_ip, client_port);}
}

此时尽管我们并没有编写客户端代码,但我们可以使用telnet命令远程登录到该服务器,因为telnet底层实际采用的就是TCP协议。
在这里插入图片描述

此时我们就可以看见客户端输入数据以后,服务端就将数据接收到并打印出来,而且服务端还将数据发送回客户端并进行打印。

我们还会发现,为该连接提供服务的套接字对应的文件描述符就是4。因为0、1、2是默认打开的,其分别对应标准输入流、标准输出流和标准错误流,而3号文件描述符在初始化服务器时分配给了监听套接字,因此当第一个客户端发起连接请求时,为该客户端提供服务的套接字对应的文件描述符就是4。

单执行流服务器的弊端

上面我们使用一个客户端链接服务器时,服务端与客户端之间可以正常通信,可是当我们在打开一个客户端以后,此时虽然在客户端显示连接是成功的,但这个客户端发送给服务端的消息既没有在服务端进行打印,服务端也没有将该数据回显给该客户端。

在这里插入图片描述
只有当第一个客户端退出后,服务端才会将第二个客户端发来是数据进行打印,并回显该第二个客户端。

在这里插入图片描述
通过上述实验现象就可以发现,对于单执行流服务器来说,我们必须是一个客户端退出后才会运行另一个客户端,而一个客户端正在运行而另一个客户端连接成功的原因就在于底层我们实际上会维护一个链接队列,服务端没有accept的新连接就会放到这个连接队列当中,当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上来罢了,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。

由于单执行流的弊端,我们就需要听过多进程或者是多线程来解决。

多进程版TCP网络程序

我们可以将当前的单执行流服务器改为多进程版的服务器,当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务,而是当前执行流调用fork函数创建子进程,然后让子进程为父进程获取到的连接提供服务。

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

我们知道,子进程创建成功以后,父子进程共享一个文件描述符表,但是父进程创建子进程后,由于进程间独立性,父子进程之间并不会相互影响,所以,父进程文件描述符的变化并不会影响子进程,就像匿名管道一样,父进程先调用pipe函数得到两个文件描述符,一个是管道读端的文件描述符,一个是管道写端的文件描述符,此时父进程创建出来的子进程就会继承这两个文件描述符,之后父子进程一个关闭管道的读端,另一个关闭管道的写端,这时父子进程文件描述符表的变化是不会相互影响的,此后父子进程就可以通过这个管道进行单向通信了。

对于套接字文件也是一样的,父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作,进而完成对对应客户端的服务,此时父进程关闭监听套接字返回的文件描述符,并不会对子进程产生影响;

等待子进程问题

子进程创建成功以后,父进程需要等待子进程退出,要不就会造成子进程成为僵尸进程,而造成内存泄漏;

但是此时不论是阻塞等待还是非阻塞等待都会存在一定的问题:

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

不等待子进程退出方式

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

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

捕捉SIGCHLD信号

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

void start()
{signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出,自动释放自己僵尸状态while (true){// sleep(1);// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = htons(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, sereversock:%d | %s | %d", serversock, client_ip.c_str(), client_port);//(2) 多进程版本pid_t id = fork();assert(id != -1);if(id == 0){close(listensock);service(serversock, client_ip, client_port);exit(0);}close(serversock);}
}

创建监控脚本,此时对代码进行测试,当服务端此时运行起来时,此时服务进程只有一个,该服务进程就是不断获取新连接的进程,而获取到新连接后也是由该进程创建子进程为对应客户端提供服务的。
在这里插入图片描述
此时创建客户端,我们会发现运行一个客户端,此时服务进程就会调用fork函数创建出一个子进程,由该子进程为这个客户端提供服务,再有一个客户端连接服务器,此时服务进程会再创建出一个子进程,让该子进程为这个客户端提供服务。

在这里插入图片描述

并且此时由于这两个客户端分别由两个不同的执行流提供服务,因此这两个客户端可以同时享受到服务,它们发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应。

在这里插入图片描述

当客户端一个个退出后,在服务端对应为之提供服务的子进程也会相继退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接。

在这里插入图片描述

让孙子进程提供服务

命名说明:

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

我们让爸爸进程创建完孙子进程后就立即退出,此时爷爷进程调用wait/waitpid函数等待爸爸进程就会立即成功,就可以继续调用accept函数获取其他客户的连接请求;

由于爸爸进程创建完孙子进程后就立马退出,此时孙子进程就变成了孤儿进程,就会被1号进程领养,当孙子进程完成客户端的需求以后,就会被系统所回收,并不需要爷爷进行的等待;

关闭对应的文件描述符

因为创建完子进程以后父子进程独立,所以关闭父进程文件描述符并不会影响子进程,同样,对于子进程来说,他也并不关心监听套接字,因此也可以将监听套接字关闭掉;

对于服务进程来说,当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),如果服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少。

而对于爸爸进程和孙子进程来说,还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作,此时就会对监听套接字当中的数据造成影响。

void start()
{// signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出,自动释放自己僵尸状态while (true){// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = htons(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, sereversock:%d | %s | %d", serversock, client_ip.c_str(), client_port);// 开始通信//(2.1) 孙子进程提供服务pid_t id = fork();assert(id != -1);if(id == 0){close(listensock);if(fork() > 0) exit(0);service(serversock, client_ip, client_port);exit(1);}waitpid(id, nullptr, 0);close(serversock);}
}

创建监控脚本,此时创建客户端,我们会发现运行一个客户端,此时爸爸进程就会调用fork函数创建出一个孙子进程,由该孙子进程为这个客户端提供服务,再有一个客户端连接服务器,此时爸爸进程会再创建出一个孙子进程,让该孙子进程为这个客户端提供服务,孙子进程的PID为1,表示他是孤儿进程,被1号进程领养。
在这里插入图片描述
并且此时由于这两个客户端分别由两个不同的孤儿进程提供服务,因此这两个客户端可以同时享受到服务,它们发送给服务端的数据都能够在服务端输出,并且服务端也会对它们的数据进行响应。
在这里插入图片描述
当客户端一个个退出后,在服务端对应为之提供服务的孙子进程也会相继退出,但无论如何服务端都至少会有一个服务进程,这个服务进程的任务就是不断获取新连接。
在这里插入图片描述

多线程版的TCP网络程序

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

在调用accept函数以后,主线程就会创建一个新线程,此时新线程就会为服务端提供服务;但是主线程此时依然会等待新线程退出,如果主线程退出了,也会造成僵尸问题。此时我们就可以调pthreda_deach函数来进行分离线程,当线程分离以后,主线程退出就不会影响新线程了。

文件描述符是否可以关闭

对于主线程和新线程来说,他们是共享一张文件描述符表的,对于新线程来说,主线程如果关闭其文件描述符,新线程也会随之关闭,所以文件描述符并不可以被主线程关闭,只有当新线程为客户端提供服务完成以后,才可以关闭文件描述符。

同样,监听套接字也不能新线程关闭,因为主线程需要通过监听套接字来获取连接,此时如果关闭了,就会造成主线程无法获取连接了。

实际新线程在为客户端提供服务时就是调用service函数,而调用service函数时是需要传入三个参数的,分别是客户端对应的套接字、IP地址和端口号。因此主线程创建新线程时需要给新线程传入三个参数,而实际在调用pthread_create函数创建新线程时,只能传入一个类型为void*的参数。

这时我们可以设计一个参数结构体ThreadData,此时这三个参数就可以放到ThreadData结构体当中,当主线程创建新线程时就可以定义一个ThreadData对象,将客户端对应的套接字、IP地址和端口号设计进这个ThreadData对象当中,然后将ThreadData对象的地址作为新线程执行例程的参数进行传入。

此时新线程在执行例程当中再将这个void类型的参数强转为ThreadData类型,然后就能够拿到客户端对应的套接字,IP地址和端口号,进而调用service函数为对应客户端提供服务。

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{char buffer[1024];while (true){ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0) // 将发过来的数据当做字符串{buffer[s] = 0;std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;}else if (s == 0){logMessage(NORMAL, "%s:%d shutdown, me too!!!", clientip.c_str(), clientport);break;}else{logMessage(errno, "read socket error:%d %s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}close(sock);
}class ThreadData
{
public:ThreadData(int sock, std::string ip, uint16_t port) : _sock(sock), _ip(ip), _port(port){}~ThreadData(){}public:int _sock;std::string _ip;uint16_t _port;
};class TcpServer
{
private:const static int gbacklog = 20;static void *threadRountine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);service(td->_sock, td->_ip, td->_port);delete td;return nullptr;}public:TcpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), listensock(-1){}void Serverinit(){// 1.创建socketlistensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error:%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "create socket sucess:%d", listensock);// 2. bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error:%d:%s", errno, strerror(errno));exit(2);}// 3. 建立连接if (listen(listensock, gbacklog) < 0){logMessage(FATAL, "listen error:%d:%s", errno, strerror(errno));exit(3);}logMessage(NORMAL, "init server sucess");}void start(){while (true){// sleep(1);// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = ntohs(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, sereversock:%d | %s | %d", serversock, client_ip.c_str(), client_port);// 开始通信// (3)多线程版本ThreadData *td = new ThreadData(serversock, client_ip, client_port);pthread_t tid;pthread_create(&tid, nullptr, threadRountine, td);//close(serversock);}~TcpServer(){}private:uint16_t _port;std::string _ip;int listensock;
};

注意

这儿的threadRountine与service需要设置为静态函数,因为类内成员函数都会传递一个this指针,此时操作系统就会识别不了。

客户端创建套接字

我们在调用socket函数创建套接字过程与服务端是一样的,但是服务端是不需要进行绑定和监听的,服务端并不会去获取连接,所以也就不需要进行监听。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cstdio>void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[1]);exit(1);}uint16_t server_port = atoi(argv[2]);std::string server_ip = argv[1];//1.创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){std::cerr << "socket error" << std::endl;exit(2);}return 0;
}

客户端链接服务器

connet

发起连接请求的函数叫做connect,该函数的函数原型如下:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。
//2.客户端连接服务器
struct sockaddr_in server;
memset(&server, 0, sizeof(server));server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{std::cerr << "connect error" << std::endl;exit(3);
}std::cout << "connect success" << std::endl;

客户端发起请求

我们实现一个简单的回声服务器,因此当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用send函数向套接字当中写入数据即可。

当客户端将数据发送给服务端后,由于服务端读取到数据后还会进行回显,因此客户端在发送数据后还需要调用recv函数读取服务端的响应数据,然后将该响应数据进行打印,以确定双方通信无误。

//客户端发起请求
while(true)
{std::string message;std::cout << "请输入# ";std::getline(std::cin, message);if(message == "quit") break;send(sock, message.c_str(), message.size(), 0);char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if(s > 0){buffer[s] = 0;std::cout << "server 回显: " << buffer << std::endl;}
}

此时我们创建监控脚本进行测试,运行服务端,通过监控可以看到,此时只有一个服务线程,该服务线程就是主线程,它现在在等待客户端的连接到来。
在这里插入图片描述
当一个客户端连接到服务端后,此时主线程就会为该客户端构建一个参数结构体,然后创建一个新线程,将该参数结构体的地址作为参数传递给这个新线程,此时该新线程就能够从这个参数结构体当中提取出对应的参数,然后调用service函数为该客户端提供服务,因此在监控当中显示了两个线程,当第二个客户端发来连接请求时,主线程会进行相同的操作,最终再创建出一个新线程为该客户端提供服务,此时服务端当中就有了三个线程。
在这里插入图片描述
这两个客户端提供服务的也是两个不同的执行流,因此这两个客户端可以同时享受服务端提供的服务,它们发送给服务端的消息也都能够在服务端进行打印,并且这两个客户端也都能够收到服务端的回显数据。

在这里插入图片描述
此时无论有多少个客户端发来连接请求,在服务端都会创建出相应数量的新线程为对应客户端提供服务,而当客户端一个个退出后,为其提供服务的新线程也就会相继退出,最终就只剩下最初的主线程仍在等待新连接的到来。
在这里插入图片描述

线程池版的TCP网络程序

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

每次主线程获取到一个连接时,就会随之创建一个新线程来给服务端提供服务,服务结束时新线程也就随之销毁,此时过程比较繁琐并且效率低下;而且如果存在需要大量线程提供服务时,随着新线程创建的越来越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也迟迟得不到应答。

解决方法

此时就可以引入我们的线程池了:

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

threadPool

#pragma once#include <iostream>
#include <vector>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"#define NUM 3template <class T>
class ThreadPool
{
public:pthread_mutex_t *getMutex(){return &lock;}bool isEmpty(){return task_queue_.empty();}void waitCond(){pthread_cond_wait(&cond, &lock);}T getTask(){T t = task_queue_.front();task_queue_.pop();return t;}private:ThreadPool(int thread_num = NUM) : num_(thread_num){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);for (int i = 1; i <= num_; i++){threads_.push_back(new Thread(i, routine, this));}}ThreadPool(const ThreadPool<T> &other) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;public:static ThreadPool<T> *getThreadPool(int num = NUM){if (thread_ptr == nullptr){LockGuard lockguard(&mutex);if (thread_ptr == nullptr){thread_ptr = new ThreadPool<T>(num);}}return thread_ptr;}// 生产void run(){for (auto &iter : threads_){iter->start();// std::cout << iter->name() << "启动成功" << std::endl;logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");}}static void *routine(void *args){ThreadData *td = (ThreadData *)args;ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;while (true){T task;{LockGuard lockguard(tp->getMutex());while (tp->isEmpty())tp->waitCond();task = tp->getTask();}// 处理任务task(td->name_);}}void pushTask(const T &task){LockGuard lockguard(&lock);task_queue_.push(task);pthread_cond_signal(&cond);}~ThreadPool(){for (auto &iter : threads_){iter->join();delete iter;}pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}private:std::vector<Thread *> threads_; // 线程组int num_;std::queue<T> task_queue_; // 任务队列pthread_mutex_t lock; // 互斥锁pthread_cond_t cond;  // 条件变量static ThreadPool<T> *thread_ptr;static pthread_mutex_t mutex;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

thread.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <cstdio>typedef void *(*func_t)(void *);class ThreadData
{
public:std::string name_;void *args_;
};class Thread
{
public:Thread(int num, func_t callback, void *args) : func_(callback){char nameBuffer[64];snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);name_ = nameBuffer;tdata_.args_ = args;tdata_.name_ = name_;}void start(){pthread_create(&tid_, nullptr, func_, (void *)&tdata_);}void join(){pthread_join(tid_, nullptr);}std::string name(){return name_;}~Thread(){}private:std::string name_; // 线程名int num_;          // 线程个数func_t func_;      // 回调函数pthread_t tid_;    // 线程IDThreadData tdata_;
};

服务类新增线程池成员

此时我们需要再线程池中新增一个指向线程池的指针成员,考虑到线程安全的问题,我们可以使用unique_ptr智能指针:

#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>
#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"class TcpServer
{
private:const static int gbacklog = 20;
public:TcpServer(uint16_t port, std::string ip = ""): _port(port), _ip(ip), listensock(-1), _threadpool_ptr(ThreadPool<Task>::getThreadPool()){}void Serverinit(){// 1.创建socketlistensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){logMessage(FATAL, "create socket error:%d:%s", errno, strerror(errno));exit(1);}logMessage(NORMAL, "create socket sucess:%d", listensock);// 2. bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if (bind(listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind error:%d:%s", errno, strerror(errno));exit(2);}// 3. 建立连接if (listen(listensock, gbacklog) < 0){logMessage(FATAL, "listen error:%d:%s", errno, strerror(errno));exit(3);}logMessage(NORMAL, "init server sucess");}void start(){_threadpool_ptr->run();// signal(SIGCHLD, SIG_IGN); //主动忽略SIGCHLD信号,子进程退出,自动释放自己僵尸状态while (true){// sleep(1);// 4. 获取连接struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){logMessage(ERROR, "accept error:%d:%s", errno, strerror(errno));continue;}// 获取连接成功uint16_t client_port = ntohs(src.sin_port);std::string client_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link success, sereversock:%d | %s | %d", serversock, client_ip.c_str(), client_port);// 开始通信// (4)线程池版本Task t(serversock, client_port, client_ip, service);_threadpool_ptr->pushTask(t);}}~TcpServer(){}private:uint16_t _port;std::string _ip;int listensock;std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

Task任务类设计

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

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

#pragma once#include <iostream>
#include <string>
#include <functional>using tfunc_t = std::function<void(int, const std::string &, uint16_t &, const std::string &)>;class Task
{
public:Task(){}Task(int sock, uint16_t port, std::string ip, tfunc_t func): _sock(sock), _port(port), _ip(ip), _func(func){}void operator()(const std::string &name){_func(_sock, _ip, _port, name);}private:int _sock;uint16_t _port;std::string _ip;tfunc_t _func;
};

假设我们此时需要做的任务是大小写切换,我们只需要更改service函数就可以,因为我们只需要将service函数当做传入我们构造的Task任务对象中,此时就会去回调我们service函数的方法:


static void change(int sock, const std::string &clientip,const uint16_t &clientport, const std::string &thread_name)
{char buffer[1024];// read && write 可以直接被使用!ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0; // 将发过来的数据当做字符串std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;std::string message;char *start = buffer;while(*start){char c;if(islower(*start)) c = toupper(*start);else c = *start;message.push_back(c);start++;}write(sock, message.c_str(), message.size());}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);}else{ logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));}close(sock);
}

客户端代码改写

因为我们此时使用的是线程池,线程池中的线程完成客户端传送过来的任务以后并不会退出,而是进入休眠状态,等待主线程连接以后下一个任务的到来,此时我们定义一个alive变量来标记我们新线程的状态,连接完成以后改变alive状态即可,此时就会进行下一个线程连接:

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " serverIp serverPort\n"<< std::endl;
}// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);bool alive = false;int sock = 0;std::string line;while (true) // TODO{if (!alive){sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0){std::cerr << "connect error" << std::endl;exit(3); // TODO}std::cout << "connect success" << std::endl;alive = false;}std::cout << "请输入# ";std::getline(std::cin, line);if (line == "quit")break;ssize_t s = send(sock, line.c_str(), line.size(), 0);if (s > 0){char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;std::cout << "server 回显# " << buffer << std::endl;}else if (s == 0){alive = false;close(sock);}}else{alive = false;close(sock);}}return 0;
}

创建监控脚本,此时运行我们服务端,会发现此时在服务端就已经有了4个线程,其中有一个是接收新连接的服务线程,而其余的3个是线程池当中为客户端提供服务的线程,我们输入一段小写字母,此时服务端接收到请求以后立马做出处理并打印出数据,此时我们客户端回显出处理结果,并进行新的连接。
在这里插入图片描述
当第二个客户端发起连接请求时,服务端也会将其封装为一个任务类塞到任务队列,然后线程池当中的线程再从任务队列当中获取到该任务进行处理,此时也是不同的执行流为这两个客户端提供的服务,因此这两个客户端也是能够同时享受服务的,当客户端退出以后,服务端也随之退出

在这里插入图片描述
与之前不同的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的3个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出。

相关文章:

网络编程套接字(二)

目录 简单的TCP网络程序服务端创建套接字服务端绑定服务端监听服务端获取连接服务端处理请求单执行流服务器的弊端 多进程版TCP网络程序捕捉SIGCHLD信号让孙子进程提供服务多线程版的TCP网络程序客户端创建套接字客户端链接服务器客户端发起请求 线程池版的TCP网络程序 简单的T…...

[极客大挑战 2019]Knife 1(两种解法)

题目环境&#xff1a; 这道题主要考察中国菜刀和中国蚁剑的使用方法 以及对PHP一句话木马的理解 咱们先了解一下PHP一句话木马&#xff0c;好吗&#xff1f; **eval($_POST["Syc"]);** **eval是PHP代码执行函数&#xff0c;**把字符串按照 PHP 代码来执行。 $_POST P…...

国家统计局教育部各级各类学历教育学生情况数据爬取

教育部数据爬取 1、数据来源2、爬取目标3、网页分析4、爬取与解析5、如何使用Excel打开CSV1、数据来源 国家统计局:http://www.stats.gov.cn/sj/ 教育部:http://www.moe.gov.cn/jyb_sjzl/ 数据来源:国家统计局教育部文献教育统计数据2021年全国基本情况(各级各类学历教育学…...

mysql、clickhouse时间日期加法

mysql 在’2023-10-27 23:59:59’上增加5秒&#xff1a; SELECT DATE_ADD(2023-10-27 23:59:59, INTERVAL 5 second);clickhouse SELECT date_add(SECOND, 3, toDate(2018-01-01 00:00:00));clickhouse时间按秒、分、时、日、月、年作差 按秒&#xff1a; SELECT dateDiff…...

21.合并两个有序链表

#include <iostream>struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {} };class Solution { public:ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {ListNode dummy ListNode(-1); // 创建一个虚拟节点作为头节点ListNode* …...

thinkphp漏洞复现

thinkphp漏洞复现 ThinkPHP 2.x 任意代码执行漏洞Thinkphp5 5.0.22/5.1.29 远程代码执行ThinkPHP5 5.0.23 远程代码执行ThinkPHP5 SQL Injection Vulnerability && Sensitive Information Disclosure VulnerabilityThinkPHP Lang Local File Inclusion ThinkPHP 2.x 任…...

暴力递归转动态规划(十三)

题目 给定3个参数&#xff0c;N&#xff0c;M&#xff0c;K 怪兽有N滴血&#xff0c;等着英雄来砍自己 英雄每一次打击&#xff0c;都会让怪兽流失[0~M]的血量 到底流失多少&#xff1f;每一次在[0~M]上等概率的获得一个值 求K次打击之后&#xff0c;英雄把怪兽砍死的概率。 暴…...

java EE 进阶

java EE 主要是学框架(框架的使用,框架的原理) 框架可以说是实现了部分功能的半成品,还没装修的毛坯房,然后我们再自己打造成自己喜欢的成品 这里学习四个框架 : Spring ,Spring Boot, Spring MVC, Mybatis JavaEE 一定要多练习,才能学好 Maven 目前我们主要用的两个功能: …...

记录paddlepaddle-gpu安装

背景 由于最近需要使用paddleocr&#xff0c;因此需要安装依赖paddlepaddle-gpu&#xff0c;不管怎么安装cuda11.6-11.8安装了一遍&#xff0c;都无法正常安装成功。如下所示&#xff1a; 环境&#xff1a;wsl2linux18.04 >>> import paddle >>> paddle.u…...

django如何连接sqlite数据库?

目录 一、SQLite数据库简介 二、Django连接SQLite数据库 1、配置数据库 2、创建数据库表 三、使用Django ORM操作SQLite数据库 1、定义模型 2、创建对象 3、查询对象 总结 本文将深入探讨如何在Django框架中连接和使用SQLite数据库。我们将介绍SQLite数据库的特点&…...

面试算法47:二叉树剪枝

题目 一棵二叉树的所有节点的值要么是0要么是1&#xff0c;请剪除该二叉树中所有节点的值全都是0的子树。例如&#xff0c;在剪除图8.2&#xff08;a&#xff09;中二叉树中所有节点值都为0的子树之后的结果如图8.2&#xff08;b&#xff09;所示。 分析 下面总结什么样的节…...

云安全-云原生k8s攻击点(8080,6443,10250未授权攻击点)

0x00 k8s简介 k8s&#xff08;Kubernetes&#xff09; 是容器管理平台&#xff0c;用来管理容器化的应用&#xff0c;提供快速的容器调度、弹性伸缩等诸多功能&#xff0c;可以理解为容器云&#xff0c;不涉及到业务层面的开发。只要你的应用可以实现容器化&#xff0c;就可以部…...

性能压力测试主要目标及步骤

性能压力测试是软件开发生命周期中至关重要的一部分&#xff0c;旨在评估应用程序或系统在高负载和极端条件下的性能表现。这种测试有助于发现性能瓶颈、资源耗尽和错误&#xff0c;以确保应用程序在真实使用情况下的可靠性和稳定性。本文将探讨性能压力测试的概念、方法和最佳…...

VLAN与配置

VLAN与配置 什么是VLAN 以最简单的形式为例。如下图&#xff0c;此时有4台主机处于同一局域网中&#xff0c;很明显这4台主机是能够直接通讯。但此时我需要让处于同一局域网中的PC3和PC4能通讯&#xff0c;PC5和PC6能通讯&#xff0c;并且PC3和PC4不能与PC5和PC6通讯。 为了实…...

API接口安全设计

简介 HTTP接口是互联网各系统之间对接的重要方式之一&#xff0c;使用HTTP接口开发和调用都很方便&#xff0c;也是被大量采用的方式&#xff0c;它可以让不同系统之间实现数据的交换和共享。 由于HTTP接口开放在互联网上&#xff0c;所以我们就需要有一定的安全措施来保证接口…...

服务器的管理口和业务口

服务器管理接口和业务接口 服务器管理接口&#xff08;Server Management Interface&#xff0c;SMI&#xff09;&#xff1a; 服务器管理接口是一种用于管理服务器硬件和操作系统的标准接口。它通常用于远程管理和监控服务器&#xff0c;包括但不限于以下功能&#xff1a; …...

【gpt redis】原理篇

用的黑马程序员redis课程的目录&#xff0c;但是不想听讲了。后续都是用gpt文档获取的。 1.课程介绍(Av766995956,P145) 2.Redis数据结构-动态字符串(Av766995956,P146) sds 1M是个界限 其实他是个由c语言实现的结构体 有这么几个参数 len alloc flag char[] len是实际长度 …...

python二次开发Solidworks:排雷以及如何排雷?

目录 1、重要文档 2、MSDN VARIANTS 3、错误排除实例 3.1 第一个例子 3.2 第二个例子 1、重要文档 SolidWorks API 帮助文档&#xff1a;在 SolidWorks 的安装路径下可以找到本地文件&#xff0c;如 ...\Program Files\SolidWorks Corp\SolidWorks\api\apihelp.chm 。 s…...

广告引擎检索技术快速学习

目录 一、广告系统与广告引擎介绍 &#xff08;一&#xff09;广告系统与广告粗分 &#xff08;二&#xff09;广告引擎在广告系统中的重要性分析 二、广告引擎整体架构和工作过程 &#xff08;一&#xff09;一般概述 &#xff08;二&#xff09;核心功能架构图 三、标…...

Scala的类和对象

1. 初识类和对象 Scala 的类与 Java 的类具有非常多的相似性&#xff0c;示例如下&#xff1a; // 1. 在 scala 中&#xff0c;类不需要用 public 声明,所有的类都具有公共的可见性 class Person {// 2. 声明私有变量,用 var 修饰的变量默认拥有 getter/setter 属性private var…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...

Ubuntu系统复制(U盘-电脑硬盘)

所需环境 电脑自带硬盘&#xff1a;1块 (1T) U盘1&#xff1a;Ubuntu系统引导盘&#xff08;用于“U盘2”复制到“电脑自带硬盘”&#xff09; U盘2&#xff1a;Ubuntu系统盘&#xff08;1T&#xff0c;用于被复制&#xff09; &#xff01;&#xff01;&#xff01;建议“电脑…...

深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向

在人工智能技术呈指数级发展的当下&#xff0c;大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性&#xff0c;吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型&#xff0c;成为释放其巨大潜力的关键所在&…...

【Post-process】【VBA】ETABS VBA FrameObj.GetNameList and write to EXCEL

ETABS API实战:导出框架元素数据到Excel 在结构工程师的日常工作中,经常需要从ETABS模型中提取框架元素信息进行后续分析。手动复制粘贴不仅耗时,还容易出错。今天我们来用简单的VBA代码实现自动化导出。 🎯 我们要实现什么? 一键点击,就能将ETABS中所有框架元素的基…...

leetcode73-矩阵置零

leetcode 73 思路 记录 0 元素的位置&#xff1a;遍历整个矩阵&#xff0c;找出所有值为 0 的元素&#xff0c;并将它们的坐标记录在数组zeroPosition中置零操作&#xff1a;遍历记录的所有 0 元素位置&#xff0c;将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

李沐--动手学深度学习--GRU

1.GRU从零开始实现 #9.1.2GRU从零开始实现 import torch from torch import nn from d2l import torch as d2l#首先读取 8.5节中使用的时间机器数据集 batch_size,num_steps 32,35 train_iter,vocab d2l.load_data_time_machine(batch_size,num_steps) #初始化模型参数 def …...

AWS vs 阿里云:功能、服务与性能对比指南

在云计算领域&#xff0c;Amazon Web Services (AWS) 和阿里云 (Alibaba Cloud) 是全球领先的提供商&#xff0c;各自在功能范围、服务生态系统、性能表现和适用场景上具有独特优势。基于提供的引用[1]-[5]&#xff0c;我将从功能、服务和性能三个方面进行结构化对比分析&#…...

Oracle实用参考(13)——Oracle for Linux物理DG环境搭建(2)

13.2. Oracle for Linux物理DG环境搭建 Oracle 数据库的DataGuard技术方案,业界也称为DG,其在数据库高可用、容灾及负载分离等方面,都有着非常广泛的应用,对此,前面相关章节已做过较为详尽的讲解,此处不再赘述。 需要说明的是, DG方案又分为物理DG和逻辑DG,两者的搭建…...