【Linux网络篇】:简单的TCP网络程序编写以及相关内容的扩展
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客
文章目录
- 一.简单的TCP网络程序
- 相关接口
- 代码实现
- 服务器单进程版
- 服务器多进程版
- 服务器多线程版
- 服务器线程池版
- 应用场景---英译汉
- 守护进程化
- 二.补充内容
- TCP建立连接(三次握手)
- TCP断开连接(四次挥手)
- TCP通信的全双工特性
一.简单的TCP网络程序
相关接口
1.socket
函数
int socket(int domain, int type, int protocol);
- 功能:创建套接字
- 参数:
domain
:协议族,比如AF_INET
(IPv4);type
:套接字类型,SOCK_DGRAM
(UDP),SOCK_STREAM
(TCP);protocol
:协议,通常为0;
- 返回值:成功返回套接字描述符
sockfd
(类似于文件描述符),后续所有的操作都依赖这个描述符;失败返回-1。
2.bind
函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:将套接字与特定的IP地址和端口绑定
-
参数:
sockfd
:套接字描述符addr
:地址结构体指针addrlen
:地址结构体长度
-
返回值:成功返回0,失败返回-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 = htonl(INADDR_ANY); // 任意IPif (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {perror("bind error");return -1; }
3.listen
函数
int listen(int sockfd, int backlog);
- 作用:服务端将套接字设置为监听状态,等待客户端连接
- 参数:
sockfd
:套接字描述符backlog
:等待连接队列的最大长度
- 返回值:成功返回0,失败返回-1。
4.accept
函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 作用:服务端接受客户端的连接请求
- 参数:
sockfd
:监听套接字描述符addr
:用于存储客户端地址信息的结构体指针addrlen
:地址结构体的长度
- 返回值:成功返回新的套接字描述符,失败返回-1。
5.connect
函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:客户端用于连接服务器
- 参数:
sockfd
:套接字描述符addr
:服务器地址信息addrlen
:地址结构体的长度
- 返回值:成功返回0,失败返回-1
7.write
函数
ssize_t write(int fd, const void *buf, size_t count);
- 作用:向文件描述符写入数据(因为TCP是面向字节流的,所以可以用该函数发送数据)。
- 参数:
fd
:文件描述符(套接字描述符)buf
:要发送的数据缓冲区count
:要发送的字节数
- 返回值:成功返回实际写入的字节数;失败返回-1,并设置
errno
。
8.read
函数:
ssize_t read(int fa, void *buf, size_t count);
- 作用:从文件描述符读取数据(因为TCP是面向字节流的,所以可以用该函数接收数据)。
- 参数:
fd
:文件描述符(套接字描述符)buf
:接收数据的缓冲区count
:缓冲区的大小
- 返回值:成功返回实际读取的字节数;失败返回-1,并设置
errno
。
9.close
函数
int close(int sockfd);
- 功能:关闭套接字
- 参数:
sockfd
:要关闭的套接字描述符
- 返回值:成功返回0;失败返回-1。
使用这些函数的基本流程:
服务端:
1.创建套接字(socket)
2.绑定套接字(bind)
3.开始监听(listen)
4.接受连接(accept)
5.接收数据(read)
6.发送数据(write)
7.关闭连接(close)
客户端:
1.创建套接字(socket)
2.连接服务器(connect)
3.发送数据(write)
4.接收数据(read)
5.关闭连接(close)
注意点:客户端并没有绑定套接字的步骤,不代表客户端不用绑定,只是不需要用户来绑定而已,这一过程是由系统来完成的。因为客户端的主要目的是连接服务器,而不是被其他的程序连接,不需要一个固定的,众所周知的端口号,系统会自定分配一个可用的临时端口号。
代码实现
主程序:
用来启动服务器
#include "tcpserver.hpp"
#include <iostream>
#include <memory>void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 2){Usage(argv[0]);exit(0);}uint16_t serverport = std::stoi(argv[1]);std::unique_ptr<TCPServer> tcp_svr(new TCPServer(serverport));tcp_svr->InitServer();tcp_svr->Run();return 0;
}
客户端:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SIZE 4096
enum{SOCK_ERR=1,CONNECT_ERR,
};void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 3){Usage(argv[0]);exit(0);}// 获取服务端的IP地址和端口号std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服务端的网络地址结构体struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));// 创建client socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "client socket create error..." << std::endl;exit(SOCK_ERR);}// 连接bind client socket 由系统完成bind 随机端口// 客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if(n < 0){std::cerr << "client connect error..." << std::endl;exit(CONNECT_ERR);}// 进行通信std::string message;while(true){std::cout << "Please Enter@ ";getline(std::cin, message);// 发送信息到服务端write(sockfd, message.c_str(), message.size());// 接收处理后的信息char buffer[SIZE];ssize_t k = read(sockfd, buffer, sizeof(buffer));if(k > 0){buffer[k] = 0;std::cout << buffer << std::endl;}else if(k == 0){std::cout << "client quit!" << std::endl;//break;}else{std::cout << "client read error!" << std::endl;//break;}}// 关闭套接字描述符close(sockfd);return 0;
}
服务器单进程版
#pragma once#include "log.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>#define MAXSIZE 4096Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}void Run(){log(INFO, "TCPServer is running...");while(true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版Service(sockfd, clientip, clientport);close(sockfd);}}~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
编译后启动服务器和客户端进行通信测试
测试现象:
先启动右上角第一个客户端进行测试,服务端以及客户端都能正常收到信息;但是当打开右下角第二个客户端进行测试时,发送两条信息后,服务端并没有正常收到信息,以及客户端也没有收到处理后的信息,而一旦把第一个客户端关闭后,第二个客户端之前发送的信息,服务端以及第二个客户端就会立即收到。
原因:
目前服务端的根据新链接建立通信这一块使用的是单进程实现的,并且是在一个循环中,只有第一个启动的客户端退出后,才能继续进入下一次的循环,重新获取新链接建立通信。所以这就是为什么第二个启动的客户端发送的信息,刚开始时服务端并没有收到,而是等到第一个客户端退出后才收到信息。
解决方法也有很多种,有多进程,多线程等方法实现。
服务器多进程版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);version 2 --- 多进程版pid_t id = fork();if(id == 0){// childclose(_listensockfd);// 子进程再创建一个新的孙子进程 然后子进程立即退出if(fork() > 0){exit(0);}// grandson// 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收Service(sockfd, clientip, clientport);close(sockfd);exit(0);}// father// 父进程关闭sockfd 然后就只剩下子进程使用sockfdclose(sockfd); // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...pid_t rid = waitpid(id, nullptr, 0); } }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
测试:
服务器多线程版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer;class _ThreadData{
public:_ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t):sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t){}public:int sockfd_;std::string ip_;uint16_t port_;TCPServer *tsvr_;
};class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}static void *Routine(void *args){// 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程pthread_detach(pthread_self());_ThreadData *td = static_cast<_ThreadData *>(args);// 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);// 释放delete td;return nullptr;}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);// version 2 --- 多进程版// pid_t id = fork();// if(id == 0){// // child// close(_listensockfd);// // 子进程再创建一个新的孙子进程 然后子进程立即退出// if(fork() > 0){// exit(0);// }// // grandson// // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收// Service(sockfd, clientip, clientport);// close(sockfd);// exit(0);// }// // father// // 父进程关闭sockfd 然后就只剩下子进程使用sockfd// close(sockfd); // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...// pid_t rid = waitpid(id, nullptr, 0); // version 3 --- 多线程版本pthread_t tid;ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);pthread_create(&tid, nullptr, Routine, td);} }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
测试:
使用多线程实现,相当于每有一个新的链接,就要创建一个新的线程,所以新线程可能会越来越多
服务器线程池版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"
#include "threadpool.hpp"
#include "Task.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer;class _ThreadData{
public:_ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t):sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t){}public:int sockfd_;std::string ip_;uint16_t port_;TCPServer *tsvr_;
};class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}static void *Routine(void *args){// 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程pthread_detach(pthread_self());_ThreadData *td = static_cast<_ThreadData *>(args);// 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);// 释放delete td;return nullptr;}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);// version 2 --- 多进程版// pid_t id = fork();// if(id == 0){// // child// close(_listensockfd);// // 子进程再创建一个新的孙子进程 然后子进程立即退出// if(fork() > 0){// exit(0);// }// // grandson// // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收// Service(sockfd, clientip, clientport);// close(sockfd);// exit(0);// }// // father// // 父进程关闭sockfd 然后就只剩下子进程使用sockfd// close(sockfd); // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...// pid_t rid = waitpid(id, nullptr, 0); // version 3 --- 多线程版本// pthread_t tid;// ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);// pthread_create(&tid, nullptr, Routine, td);// version 4 --- 线程池版本Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);} }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
线程池封装
#pragma once #include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <queue>
#include <unistd.h>struct ThreadData{pthread_t _tid;std::string _threadname;
};template<class T>
class ThreadPool{static const int defaultnum = 5;private:// 申请锁封装void Lock(){pthread_mutex_lock(&_mutex);}// 释放锁封装void UnLock(){pthread_mutex_unlock(&_mutex);}// 唤醒封装void WakeUp(){pthread_cond_signal(&_cond);}// 条件等待封装void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}// 判断任务队列是否为空封装bool IsTaskEmpty(){return _tasks.empty();}std::string GetThreadName(pthread_t tid){for(auto td : _threads){if(td._tid == tid){return td._threadname;}}return "NONE";}public:// 线程的执行函数在类内要设置成静态成员函数static void *HandlerTask(void *args){// 错误写法:线程的执行函数设置成静态成员函数后,就不能再直接访问类内的成员变量和成员函数// while(true){// Lock();// while(_task.empty()){// ThreadSleep();// }// T t = _task.front();// _task.pop();// UnLock();// t.run(); // 每个任务对象在类内有自己的执行方法,所以不用在临界区执行// }// 正确写法:ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();// 循环判断条件,防止伪唤醒while(tp->IsTaskEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->UnLock();t.Run();}}void start(){int size = _threads.size();for (int i = 0; i < size; i++){_threads[i]._threadname = "thread-" + std::to_string(i + 1);//pthread_create(&(_threads[i].tid), nullptr, HandlerTask, nullptr);// 为了方便线程执行函数直接访问成员变量和成员函数,这里将this指针作为参数传过去pthread_create(&(_threads[i]._tid), nullptr, HandlerTask, this);}}// 存放任务到任务队列中void Push(const T&t){Lock();_tasks.push(t);WakeUp();UnLock();}// 从任务队列中获取任务T Pop(){T t = _tasks.front();_tasks.pop();return t;}// 懒汉方式单例模式// 静态成员函数只能访问静态成员变量 静态成员变量只有一份,在多线程情况下,就变成了共享资源// 需要加上互斥机制进行保护static ThreadPool<T>* GetInstance(){// 因为多线程只会在初次创建单例时才会竞争_tp指针 如果直接在判断外面加锁// 会导致创建之后每次使用单例时都要申请锁释放锁 所以采用双层判断空指针 降低锁冲突的概率 提高性能if (_tp == nullptr){pthread_mutex_lock(&_s_mutex);if (_tp == nullptr){_tp = new ThreadPool<T>();}pthread_mutex_unlock(&_s_mutex);}return _tp;}private:ThreadPool(int num = defaultnum):_threads(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}// 禁用拷贝构造函数和赋值运算符// 禁止通过拷贝构造函数创建新的线程池对象ThreadPool(const ThreadPool<T> &) = delete;// 禁止通过赋值运算符将一个线程池对象那个赋值给另一个const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;private:std::vector<ThreadData> _threads; // 线程数组std::queue<T> _tasks; // 任务队列pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *_tp; // 静态成员变量static pthread_mutex_t _s_mutex;
};template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr; // 静态成员变量只能在类外初始化template <class T>
pthread_mutex_t ThreadPool<T>::_s_mutex = PTHREAD_MUTEX_INITIALIZER;
任务封装
#pragma once#include <iostream>
#include <string>
#include "log.hpp"extern Log log;class Task{
public:Task(const int sockfd, const std::string ip, const uint16_t port): _sockfd(sockfd), _ip(ip), _port(port){}void Run(){char buffer[4096];while(true){ssize_t n = read(_sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送n = write(_sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", _ip.c_str(), _port, _sockfd);}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", _sockfd, _ip.c_str(), _port);}}}~Task(){}
private:int _sockfd;std::string _ip;uint16_t _port;
};
效果和多线程实现的一样,这里就不再展示。
应用场景—英译汉
直接使用线程池版的服务器来实现:
dict.txt文件
该文件用来存放进行翻译的单词(可以自己添加对应的单词)
apple:苹果
banana:香蕉
red:红色
Init.hpp
用来封装英译汉功能:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "log.hpp"const std::string dictname = "./dict.txt";
const std::string sep = ":";static bool Split(std::string &line, std::string *part1, std::string *part2){auto pos = line.find(sep);if(pos == std::string::npos){return false;}// 输出型参数*part1 = line.substr(0, pos);*part2 = line.substr(pos + 1);return true;
}class Init{
public:Init(){std::ifstream in(dictname);if(!in.is_open()){log(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}std::string line;while (std::getline(in, line)){std::string part1, part2;Split(line, &part1, &part2);dict.insert({part1, part2});}in.close();}std::string translation(const std::string &key){auto iter = dict.find(key);if (iter == dict.end()){return "UnKnow";}else{return iter->second;}}private:std::unordered_map<std::string, std::string> dict;
};
修改Task.hpp:
将服务端对信息的处理转变为翻译:
测试:
注意点:
如果服务器处理完信息后正在向客户端写入时,此时客户端正好关闭,可能会导致服务器写入失败问题,所以在Task.hpp
中需要对服务器写入做异常处理。
如果服务器写入失败,可能会导致服务器的崩溃以及被系统杀掉,所以在服务器中需要对异常信号做忽略处理:
模拟实现客户端重连现象
为客户端添加一个重连模块:
如果客户端正在通信时,服务器关闭,客户端会重新进行链接,如果链接一定次数后还是失败,就会终止退出。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SIZE 4096
enum{SOCK_ERR=1,CONNECT_ERR,
};void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 3){Usage(argv[0]);exit(0);}// 获取服务端的IP地址和端口号std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服务端的网络地址结构体struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){// 创建client socketint sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "client socket create error..." << std::endl;exit(SOCK_ERR);}int cnt = 5;int isreconnect = false;// 连接bind client socket 由系统完成bind 随机端口do{// 客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "client connect error..., reconnect: "<< cnt << std::endl;sleep(2);}else{break;}} while (cnt && isreconnect);if(cnt == 0){std::cerr << "user ofline..." << std::endl;break;}// 进行通信std::string message;while (true){std::cout << "Please Enter@ ";getline(std::cin, message);// 发送信息到服务端ssize_t s = write(sockfd, message.c_str(), message.size());if (s < 0){std::cout << "client write error!" << std::endl;continue;}// 接收处理后的信息char buffer[SIZE];ssize_t k = read(sockfd, buffer, sizeof(buffer));if (k > 0){buffer[k] = 0;std::cerr << buffer << std::endl;}else if (k == 0){std::cout << "client reconnect!" << std::endl;break;}else{std::cerr << "client read error!" << std::endl;break;}}// 关闭套接字描述符close(sockfd);}return 0;
}
守护进程化
1.前台进程和后台进程:
在Linux中每个用户登录时,都会创建一个会话框session
,每个session
都包含一个bash
进程(命令行解释器),刚开始时,bash
进程就是前台进程。一个session
只能有一个前台进程在运行,键盘信号只能发给前台进程;至于后台进程无法从标准输入获取数据。
谁拥有键盘文件,谁就是前台进程;而前台进程和后台进程都可以向显示器文件打印。
- 以前台方式执行
./process
可以被键盘输入的ctrl+c
终止掉。
- 以后台方式执行
./process &
后台进程也是向显示器文件打印,为了防止干扰,将打印内容重定向到文件中
启动时前面的[1]
表示后台任务号。
- 查看所有的后台进程
jobs
- 将后台进程转变为前台进程
fg 后台任务号
- 暂停后台进程
先将后台进程变为前台进程,然后键盘输入ctrl+z
:
- 取消暂停
bg 后台任务号
2.Linux的进程间关系:
在上面图中,启动两个后台进程,第二个是通过管道启动三个进程,打印对应的进程信息。
其中PGID
表示当前进程所属的进程组ID;SID
表示当前进程所属的session
对话框的ID。
对于./process
进程,一个进程就是一个进程组;而对于管道方式启动的三个sleep
进程,属于同一个进程组,并且他们的进程组ID还是第一个进程的PID
。
什么是进程组:
- 一个或多个进程的集合;
- 每个进程组都有一个唯一的进程ID;
- 进程组中的第一个进程成为组长进程;
- 组长进程的PID等于进程组的PGID;
3.进程组和任务之间的关系:
在Linux中,任务和进程是同一个概念。
一个任务可以有多个进程共同完成,也可以是一个进程独立完成;而进程组既可以包含一个进程也可以包含多个进程。
一个任务只能属于一个进程组,而一个进程组可以包含多个任务。
纠正之前的概念:前台进程,后台进程实际上应该叫做前台任务和后台任务。
4.守护进程化:
在同一个session
会话中执行的指令转变为进程后,每个进程的父进程都是bash
进程:
一旦关闭bash
进程后,重新登陆建立一个新的session
会话启动一个新的bash
进程,再次查看之前的后台任务:
这些后台进程并没有因为父进程bash
的退出而终止掉,而是继续执行,并且失去父进程后,被系统领养变成孤儿进程。
结论就是:这些后台进程会受到用户登录和退出的影响!因为父进程改变!
如果想要创建一个不受到任何用户登录和退出影响的进程—需要守护进程化!
守护进程是一种特殊的后台进程,具有以下特征:
- 在后台运行
- 没有控制终端(自成进程组自成会话,不受到任何用户登录和退出的影响)
- 生命周期长
- 系统启动时运行,系统关闭时终止
因为守护进程需要创建新的会话,自成一个会话,所以父进程也是系统。
实际上守护进程也是孤儿进程,只不过这种“孤儿化”是有意为之的,目的就是让进程完全脱离控制终端,在后台长期运行。
创建新的会话需要用到setsid
函数:
pid_t setsid(void);
- 主要功能:
- 创建一个新的会话
- 调用进程成为新会话的会话首进程
- 调用进程成为新进程组的组长进程
- 调用进程没有控制终端
- 调用规则:
- 调用进程不能是进程组的组长
- 如果调用进程是进程组的组长,调用会失败
- 所以通常要先fork创建子进程,在子进程中调用该函数创建新的会话
- 作用:
- 使进程完全脱离控制终端
- 使进程成为新会话的领导者
- 使进程成为新进程组的组长
- 确保进程在后台运行
- 返回值:
- 成功:返回新会话的ID
- 失败:返回-1,并设置errno
- 常见用途:
- 创建守护进程
- 实现后台服务
- 脱离终端控制
- 创建独立进程组
将服务器守护进程化:
daemon.hpp
:
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = ""){// 1. 创建子进程pid_t id = fork();if(id > 0){// 父进程直接终止退出exit(0);}// 2. 子进程创建新的会话setsid();// 3. 更改当前调用进程的工作目录if (!cwd.empty()){chdir(cwd.c_str());}// 4. 标准输入 标准输出 标准错误重定向到/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}// 5. 忽略异常信号signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);
}
tcpserver.hpp
:
重新编译执行:
启动后的服务器的PID
,进程组PGID
,会话SID
都是同一个4725
,说明当前的服务器是自成进程组自成会话的一个后台进程。
因为这里启动时并没有设置指定的工作路径,所以启动后还是在当前调用进程的工作目录下。
查看一下服务器的三个标准流是否重定向到目标文件/dev/null
下;该文件是一个特殊的设备文件,可以理解为一个垃圾站,会自动丢弃写入的数据。
测试时,左边终端启动完服务器后关闭,然后在右边打开一个新的终端,启动客户端然后进行测试:
再通过指令查看之前的服务器:
通过信号直接杀掉服务器后再次查看,就找不到之前启动的服务器了:
如果不想手写守护进程化的代码,可以直接使用系统库中的daemon
函数:
函数原型:
int daemon(int nochdir, int noclose);
参数说明:
nochdir
:0
:将工作目录改为根目录"/"
;1
:保持当前工作目录不变;
noclose
:0
:将标准输入,输出,错误重定向到/dev/null
;1
:保持标准输入输出,错误不变;
二.补充内容
TCP建立连接(三次握手)
目的:让客户端和服务器双方都确认彼此的收发功能正常,建立可靠的连接。
步骤:
-
1.第一次握手:客户端—>服务器
客户端发送
SYN
(同步)包,告诉服务器“我要连接你”。 -
2.第二次握手:服务器—>客户端
服务器收到后,回复
SYN+ACK
包,表示“我收到了你的请求,我也准备好了”。 -
第三次握手:客户端—>服务器
客户端收到
SYN+ACK
后,再发一个ACK
包,表示“我也准备好了”。
结果:连接建立,双方可以开始通信了。
形象比喻:
- 客户端敲门(
SYN
) - 服务器回应“我在家,你是谁?”(
SYN+ACK
) - 客户端说“我是XX,咱们聊聊吧!”(
ACK
)
TCP断开连接(四次挥手)
目的:让双方都能优雅的关闭连接,确保数据都传完。
步骤:
-
1.第一次挥手:客户端—>服务器
客户端发送
FIN
包,表示“我没有数据要发了,可以断开了”。 -
2.第二次挥手:服务器—>客户端
服务器收到后,回复
ACK
包,表示“我知道了”。 -
3.第三次挥手:服务器—>客户端
服务器处理完自己的数据后,也发送
FIN
包,表示“我也没有数据要发了”。 -
4.第四次挥手:客户端—>服务器
客户端收到
FIN
后,回复ACK
包,表示“我知道了”。
结果:连接彻底关闭。
形象比喻:
- 客户端说”我说完了“(
FIN
) - 服务器点头“我知道了”(
ACK
) - 服务器说“我也说完了”(
FIN
) - 客户端点头“我知道了”(
ACK
)
总结口诀
- 三次握手:你来——我应——你再确认
- 四次挥手:你说完——我知道——我也说完——你知道
TCP通信的全双工特性
先用一个简单的类比来理解什么是TCP全双工通信:
想象两个人通过电话通话:
1.双方可以同时说话和听对方说话
2.不需要等待对方说完才能说话
3.双方都有独立的“说话通道”和“听通道”
在TCP通信中:
1.每个TCP连接都有两个独立的数据流:
- 一个从客户端到服务器
- 一个从服务器到客户端
2.这两个数据流可以同时工作,互不影响
3.不需要等待一个方向的数据传输完成才能开始另一个方向
而TCP通信的全双工特性与缓冲区机制密切相关:
1.每个TCP连接都有四个缓冲区:
- 发送方的发送缓冲区
- 发送方的接收缓冲区
- 接收方的发送缓冲区
- 接收方的接收缓冲区
2.缓冲区的作用:
- 发送缓冲区:存储待发送的数据
- 接收缓冲区:存储已接收但还未被应用程序读取的数据
3.为什么能实现全双工:
- 发送和接收使用不同的缓冲区
- 发送操作不会阻塞接受操作
- 接受操作不会阻塞发送操作
明白了上面的特性之后,再来思考一个问题:
TCP是面向字节流的,那如何保证,读取上来的数据,是“一个完整”的报文呢?
首先,我们使用的read
和write
函数只是拷贝函数。包括send
和recv
函数也是如此,这些函数并不是网络数据的收发函数,只是实现数据从用户到内核或者从内核到用户的拷贝。
先来理解发送数据:
write
发送数据实际上是将数据拷贝到发送方的发送缓冲区,具体决定网络收发的(什么时候发,发多少,出错了怎么办)是TCP协议决定的。
因为只有TCP协议了解当前网络的健康状态和接收方的接受能力(之后会讲解),所以必须由TCP协议决定网络数据的收发。
而TCP协议属于操作系统的网络模块部分,所以用户把数据交给TCP,本质上就是把数据交给操作系统。就和之前学习文件时的把数据拷贝到struct *file
指向的文件缓冲区里一样;把数据发出去,发送到对应端的网络里,这个过程也如同当初学习文件时,把数据从文件缓冲区刷新到磁盘一样;作用都是一样的。
再来理解接收数据:
read
接收数据实际上是将数据从接收方的接收缓冲区拷贝到用户自定义的缓冲区,而对方发送多少数据,什么时候发的,有没有一次性全部发送过来这些都是由操作系统(TCP协议)来决定的;至于接收方从接收缓冲区中读取时,读取多少,什么时候读取,一次性读取完还是分批次读取这些都是不确定的了!
所以在接收方的用户应用层中就必须保证把协议定好,这样才能更好的定义读取到的数据分析,保证读取到的是“一个完整”的报文!
所以接下来的学习内容就是用户应用层的协议!
以上就是关于TCP网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
相关文章:

【Linux网络篇】:简单的TCP网络程序编写以及相关内容的扩展
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:Linux篇–CSDN博客 文章目录 一.简单的TCP网络程序相关接口代码实现服务器单进程版服务器多…...
22.代理模式:思考与解读
原文地址:代理模式:思考与解读 更多内容请关注:深入思考与解读设计模式 引言 在软件开发中,尤其是当对象的访问需要控制时,你是否遇到过这样的问题:某些操作或对象可能需要进行额外的检查、优化或延迟加载ÿ…...

Scratch节日 | 粽子收集
端午节怎么过?当然是收粽子啦!这款 粽子收集 小游戏,让你一秒沉浸节日氛围,轻松收集粽子,收获满满快乐! 🎮 玩法介绍f 开始游戏:点击开始按钮,游戏正式开始!…...

stl三角面元文件转颗粒VTK文件
效果展示: import os import sys import json import argparse import numpy as np import pandas as pd import open3d as o3d from glob import globPARTICLE_RADIUS 0.025def stl_to_particles(objpath, radiusNone):if radius is None:radius PARTICLE_RADIU…...

Java String的使用续 -- StringBuilder类和StringBuffer
文章目录 字符串的不可变性StringBuilder和StringBuffer函数使用 字符串的不可变性 字符串不可变是因为有private修饰,只能在类的内部使用不可以在类外使用,因此使用时是不可以修改字符串的 public class test {public static void main(String[] args…...
Android学习之定时任务
Android定时任务的实现方式 在Android开发中,定时任务主要可以通过以下两类方式实现: Android系统组件 Handler消息机制:通过Handler.postDelayed()实现延时任务,适合简单UI线程操作AlarmManager:系统级定时服务&…...
WEB安全--RCE--webshell HIDS bypass4
继WEB安全--RCE--webshell HIDS bypass3的补充: 十三、时间开关 webshell: <?php ini_set("display_errors",1); function foo($test, $bar FSYSTEM) {echo $test . $bar; } $function new ReflectionFunction(foo); $q new ParseEr…...

基于python+Django+Mysql的校园二手交易市场
文章目录 基于pythonDjangoMysql的校园二手交易市场运行步骤系统设计功能设计任务目标用户特点参与者列表基本要求功能模块图 数据库设计会员用户信息表(user_userinfo)商品信息表(goods_goodsinfo)管理员用户信息表(a…...

从零打造算法题刷题助手:Agent搭建保姆级攻略
我用Trae 做了一个有意思的Agent 「大厂机试助手」。 点击 https://s.trae.com.cn/a/d2a596 立即复刻,一起来玩吧! Agent 简介 Agent名称为大厂机试助手,主要功能有以下三点。 解题: 根据用户给出的题目给出具体的解题思路引导做…...
Oracle 12c新增的数字转换验证VALIDATE_CONVERSION函数
Oracle 12c新增的数字转换验证函数 一、VALIDATE_CONVERSION函数(12c R2新增) Oracle 12c Release 2引入了原生验证函数,可直接判断字符串能否转换为指定类型: SELECT VALIDATE_CONVERSION(123.45 AS NUMBER) FROM dual; -- 返…...
参数/非参数检验和连续/离散/分类等变量类型的关系
参数统计方法通常应用于参数变量,但参数变量并不都是连续型变量。参数变量是指那些可以用参数(如均值、方差等)来描述其分布特征的变量。参数变量可以是连续型变量,也可以是离散型变量,只要它们遵循某种特定的分布&…...

懒人云电脑方案:飞牛NAS远程唤醒 + 节点小宝一键唤醒、远程控制Windows!
后台高频问题解答: “博主,飞牛NAS能定时开关机了,能不能让它顺便把家里Windows电脑也远程唤醒控制?最好点一下就能连,不用记IP端口那种!” 安排!今天这套方案完美实现: ✅ 飞牛NAS…...

【Python】第一弹:对 Python 的认知
目录 一、Python 的背景 1.1. Python 的由来 1.2 Python 的作用 1.3 Python 的优缺点 1.4 Python 的开发工具 一、Python 的背景 1.1. Python 的由来 Python 由荷兰数学和计算机科学研究学会的吉多・范罗苏姆 (Guido van Rossum)在 20 世纪 80 年代…...

直播预告 | 聚焦芯必达|打造可靠高效的国产 MCU 与智能 SBC 汽车解决方案
随着汽车电子国产化快速推进,车规级 MCU 与 CAN/LIN SBC 作为车身控制的核心组件,正面临更高的安全与可靠性挑战。品佳集团将携手芯必达微电子,深入剖析国产 MCU/SBC/智能 SBC 的最新技术与应用,助力企业打造高性能、可量产的国产…...

Java源码中有哪些细节可以参考?(持续更新)
欢迎来到啾啾的博客🐱。 记录学习点滴。分享工作思考和实用技巧,偶尔也分享一些杂谈💬。 有很多很多不足的地方,欢迎评论交流,感谢您的阅读和评论😄。 目录 String的比较final的使用transient避免序列化 St…...

GelSight Mini触觉传感器:7μm精度+3D 映射,赋能具身智能精密操作
GelSight Mini 高分辨率视触觉传感器采用先进的光学成像与触觉感知技术,赋予机器人接近人类的触觉能力。该设备可捕捉物体表面微观细节,并生成高精度的2D/3D数字映射,帮助机器人识别形状、纹理及接触力,从而执行更复杂、精准的操作…...

day 23 机器学习管道(pipeline)
在机器学习领域,“pipeline” 常被翻译为 “管道” 或 “流水线”,它是机器学习中极为重要的概念。在构建机器学习模型时,通常需按特定顺序对数据执行预处理、特征提取、模型训练以及模型评估等步骤,而使用 “pipeline” 能有效管…...
shell编程笔记
变量定义 在 Shell 中,变量定义无需声明类型,直接赋值即可。变量名区分大小写,建议使用大写字母命名环境变量,小写字母命名局部变量。赋值时等号()两边不能有空格。 MY_VAR"Hello World" # 定…...

鸿蒙仓颉开发语言实战教程:自定义组件
关于仓颉开发语言我们已经连续分享了很多天,相信大家对于仓颉开发语言已经有了一定的了解。今天我们继续进阶,分享一个仓颉开发语言中的自定义组件知识。 本文案例就以上一篇文章中的自定义tabbar为例,因为我们自己开发的tabbar一直放在inde…...

基于Spring Boot+Vue 网上书城管理系统设计与实现(源码+文档+部署讲解)
技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…...

opencvsharp usb摄像头录像 c# H264编码
1.首先创建ConsoleApp,.Net 9.0,不要创建WinForm 。WInForm帧率和实际对不上,有延时。 2.下载opencvsharp。 3.下载openh264-1.8.0-win32.dll , openh264-1.8.0-win64.dll .放在根目录。 https://github.com/cisco/openh264 using OpenCv…...
ch12 课堂参考代码 及 题目参考思路
课堂参考代码 Bellman-Ford 主要思路:对所有的边进行 n-1 轮松弛操作 单源最短路算法, O ( n m ) O(nm) O(nm) using ll long long; const int maxn 5010, maxm 5010; struct Edge {int u, v, w; } E[maxm]; // d[u]: 当前 s 到 u 的最短路 ll d[m…...
uniapp 实现腾讯云 IM 消息已读回执
uniapp 实现腾讯云 IM 消息已读回执处理全攻略 一、功能实现原理 腾讯云 IM 的已读回执功能通过 消息已读上报机制 实现,核心流程如下: 接收方阅读消息时,客户端自动上报已读状态云端记录最新已读时间戳(精确到会话维度&#x…...

JavaScript 性能优化按层次逐步分析
JavaScript 性能优化实战 💡 本文数据基于Chrome 136实测验证,涵盖12项核心优化指标,通过20代码案例演示性能提升300%的实战技巧。 一、代码层深度优化 1. 高效数据操作(百万级数据处理) // 不良实践:频繁…...
三分钟打通Stable Diffusion提示词(附实战手册)
文章目录 一、提示词的本质是"思维翻译器"避坑指南1:三大常见翻车现场 二、结构化提示词公式(抄作业版)实战案例:生成赛博朋克猫咪 三、进阶玩家的秘密武器1. 权重控制大法2. 风格融合黑科技3. 时间轴控制 四、避不开的…...

【Linux网络篇】:初步理解应用层协议以及何为序列化和反序列化
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨ ✨ 个人主页:余辉zmh–CSDN博客 ✨ 文章所属专栏:Linux篇–CSDN博客 文章目录 一.序列化和反序列化为什么需要序列化和反序列化为什么应用层…...
RK3588 Opencv-ffmpeg-rkmpp-rkrga编译与测试
RK3588 Opencv-ffmpeg-rkmpp-rkrga编译与测试 硬件背景说明编译环境准备1. 编译MPP(媒体处理平台)2. 编译RGA(图形加速库)3. 构建支持硬件加速的FFmpeg重要代码修改说明4. 验证安装5.FFmpeg转码测试OpenCV编译集成Python OpenCV+FFmpeg测试硬件背景说明 RK3588是瑞芯微推出…...

特伦斯 S75 电钢琴:奏响极致音乐体验的华丽乐章
在音乐爱好者增多、音乐教育普及,以及科技进步的推动下,电钢琴市场蓬勃发展。其在技术、品质和应用场景上变化巨大,高端化、个性化产品受青睐,应用场景愈发多元。在此背景下,特伦斯 S75 电钢琴以卓越性能和独特设计&am…...

硬件学习笔记--64 MCU的ARM核架构发展及特点
MCU(微控制器)的ARM核架构是当前嵌入式系统的主流选择,其基于ARM Cortex-M系列处理器内核,具有高性能、低功耗、丰富外设支持等特点。以下是ARM核MCU的主要架构及其发展: 1. ARM Cortex-M系列内核概览 ARM Cortex-M系…...
div或button一些好看实用的 CSS 样式示例
1:现代渐变按钮 .count {width: 800px;background: linear-gradient(135deg, #72EDF2 0%, #5151E5 100%);padding: 12px 24px;border-radius: 10px;box-shadow: 0 4px 15px rgba(81, 81, 229, 0.3);color: white;font-weight: bold;border: none;cursor: pointer;t…...