网络编程套接字【端口号/TCPUDP/网络字节序/socket编程接口/UDPTCP网络实验】
网络编程套接字
- 0. 前言
- 1. 认识端口号
- 2. 认识TCP和UDP协议
- 3. 网络字节序
- 4. socket编程接口
- 5. 实现一个简单的UDP网络程序
- 5.1 需求分析
- 5.2 头文件准备
- 5.3 服务器端设计
- 5.4 客户端设计
- 5.5 本地测试
- 5.6 跨网络测试
- 5.7 UDP小应用——客户端输入命令,服务器端执行
- 6. 地址转换函数
- 7. 实现一个简单的TCP网络程序
- 7.1 V1版本——bug版
- 7.1.1 服务器端
- 7.1.2 客户端
- 7.2 V2版——多进程,wait版
- 7.3 V3版——多进程信号版
- 7.4 V4——多线程版
- 8. 如何理解:面向数据报&&面向字节流
0. 前言
学习本章,需要先了解日志:学习日志。并且要对Linux系统有一定的了解,遇到知识盲区可以去看我的Linux专栏。
1. 认识端口号
1. 思考
- 网络通讯的最终目的,是主机和主机之间通信吗?答案是:不仅仅是。最终目的是两台主机中的两个进程间通信。
- 我们所有的网络通信行为,本质都是进程间通信!!!
- 第一步首先要将数据送达目标机器,第二步则是找到指定进程;
- IP地址用于标识互联网中唯一一台主机,端口号(port)则用来标识主机中唯一一个进程;
{ip:port}
组合,可以标识互联网中唯一一个进程,这个组合也叫套接字,socket(这个英文单词有插座、插孔的意思)。
2. 端口号(port)
- 端口号是传输层协议的内容;
- 端口号是一个2字节16比特的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用;但一个进程可以有多个端口号。
3. 端口号 VS pid
pid
就可以标识一台机器中的唯一一个进程,为什么还要设计端口号?pid
是系统中设计的一个标识,如果有一天,系统的设计规则改变了,那么将会直接影响网络的设计。为了将系统和网络进行解耦,又设计了一个端口号的概念,独属于网络范畴。
2. 认识TCP和UDP协议
这里我们先对两个协议有一个直观认识,后面再讨论细节。
1. TCP
- 传输层协议;
- 有连接;
- 可靠传输;
- 面向字节流。
2. UDP
- 传输层协议;
- 无连接;
- 不可靠传输;
- 面向数据报。
3. 可靠传输 VS 不可靠传输
- “可靠”在这里是一个中性词,没有好坏之分;
- 对于可靠传输,可能会存在丢包检测的操作,一旦发现丢包就重发数据,这也必然会消耗更多的资源和时间;
- 不可靠传输则不会做这些检测,对于处于良好网络环境的两台机器,丢包的检测倒显得次要了。并且不可靠传输的实现也较为简单,在一些场景下会有奇效;
- 例如在直播领域,画面偶尔卡顿一下,声音炸一下,影响其实不是很大,所以UDP协议也广泛应用在直播领域。
对于其他的概念,有连接和无连接,面向字节流和面向数据报,我们之后再详谈。
3. 网络字节序
1. 大端机和小端机
- 大端机:高权值位存在低地址处;
- 小端机:高权值位存在高地址处。
2. 问题引入
- 发送主机经常将发送缓冲区中的数据按照内存地址从低到高的顺序发出;
- 接收主机把从网络中接收的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序保存的;
- 此时如果一个大端机要和小端机进行网络通信,由于彼此的存储方案不同,小端机收到的数据很可能是相反的,如何解决?
3. 网络字节序
- 为了解决上述问题,统一规定,发送进网络中的数据,必须是大端的;这也就意味着,所有从网络中收到数据的机器,都会知道数据是大端的;
- 规定网络中的数据是大端,而不是小端,没有什么特别的理由。如果硬要找一个理由,就是按照大端方式存储的数据,可读性较强。
4. 一批和网络字节序有关的接口
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong); // 4字节主机ip序列转网络序列
uint16_t htons(uint16_t hostshort); // 2字节主机端口序列转网络序列
uint32_t ntohl(uint32_t netlong); // 4字节网络ip序列转主机序列
uint16_t ntohs(uint16_t netshort); // 2字节网络端口序列转主机序列
- 其中,h就是host主机的意思,n是net网络的意思,l就是long,s就是short。
4. socket编程接口
1. socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 网络编程时,socket有很多类别:
- unix socket:域间socket,使用同一台机器上的文件路径,类似于命名管道,用来进行本主机内部的通信;
- 网络socket:使用ip+端口号,进行网络通信;
- 原始socket:用来编写一些网络工具。
- 理论上而言,我们应该给上述每种应用场景都设计一套编程接口,但事实是,他们共用一套编程接口。这是如何实现的?
2. sockaddr结构
- socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
- IPv4和IPv6的地址格式定义在
netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位地址类型,16位端口号和32位IP地址; - IPv4、IPv6地址类型分别定义为常数
AF_INET
、AF_INET6
。这样,只要取得某种sockaddr
结构体的首地址,不需要知道具体是哪种类型的sockaddr
结构体,就可以根据地址类型字段确定结构体中的内容,自动强制转化; struct sockaddr
是一个通用地址类型,在使用时需要强制转化;这样的好处是提高了程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的sockaddr
结构体指针做为参数。这是C风格的多态;- 之所以没有使用
void
作为通用地址类型,是因为这批接口被设计出来时,C语言还不支持void*
。
3. sockaddr
/* Structure describing a generic socket address. */
struct sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};
4. sockaddr_in
/* Structure describing an Internet socket address. */
struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];};
- 虽然socket api的接口是
sockaddr
,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in
; - 这个结构里主要有三部分信息:
- 地址类型
sin_family
,一个宏; - 端口号
sin_port
,uint16_t
类型,typedef uint16_t in_port_t;
; - IP地址
sin_addr
,struct in_addr
类型(一会儿还要看这个类型里有什么)。
- 地址类型
有的同学可能有疑问,哪里有
sin_family
?我们再转到宏定义,看一下它是怎么实现的:#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
这里就要考验大家的C语言功底了,已知有
typedef unsigned short int sa_family_t;
,sa_prefix
是一个占位符,传的是sin_
,##
表示预编译时,和后面的内容拼接,所以就有了sin_family
。
5. in_addr
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr{in_addr_t s_addr;};
in_addr.s_addr
用来表示一个IPv4的IP地址,其实就是一个32位的整数。
5. 实现一个简单的UDP网络程序
5.1 需求分析
1. 服务器端
- 服务器端接收客户端发来的消息,并将消息再发回给客户端;
- 服务器接收消息时,也想知道客户端的端口号和ip地址。
2. 客户端
- 客户端向服务器端发消息,并接收服务器端发来的消息。
5.2 头文件准备
1. nocpoy类——nocopy.hpp
- 继承该类的子类,统统不能拷贝。
#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};
2. 错误信息设计——Comm.hpp
#pragma onceenum{Usage_Err = 1, // 使用方式错误Socket_Err, // Socket错误Bind_Err // Bind错误
};
3. 网络地址类设计——InetAddr.hpp
- 网络编程中,经常会遇到打印或保存网络地址的需求,所以提前把网络地址类给封装好。
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{
public:InetAddr(struct sockaddr_in &addr){_port = ntohs(addr.sin_port); _ip = inet_ntoa(addr.sin_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port;}std::string PrintDebug(){std::string info = _ip;info += ":";info += std::to_string(_port);return info;}~InetAddr(){}private:std::string _ip; // ipuint16_t _port; // 端口号
};
下面两个类设计在我们Linux专栏中都有讲到,感兴趣的小伙伴可以学习一下。
4. 日志类——Log.hpp
#pragma once#include<iostream>
#include<string>
#include<cstdarg>
#include<ctime>
#include<fstream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include"LockGuard.hpp"enum
{Debug = 0,Info, // 正常信息Warning, // 告警Error, // 错误Fatal // 致命错误
};enum
{Screen = 0, // 向显示器打印OneFile, // 向一个文件打印ClassFile // 向多个文件打印
};// 将日志等级转换为string
std::string LevelToString(int level)
{switch(level){case Debug:return "Debug";case Info:return "Info";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "Unknown";}
}const int defaultstyle = Screen; // 默认风格是向显示器打印
const std::string default_filename = "log."; // 默认文件名
const std::string logdir = "log"; // 默认日志文件夹class Log
{
public:Log():_style(defaultstyle),_filename(default_filename){mkdir(logdir.c_str(), 0775);pthread_mutex_init(&_mutex, nullptr);}void Enable(int style){_style = style;}std::string TimeStampExLocalTime(){time_t currtime = time(nullptr);struct tm *curr = localtime(&currtime);char time_buffer[128];snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d", \curr->tm_year+1900, curr->tm_mon+1, curr->tm_mday,\curr->tm_hour, curr->tm_min, curr->tm_sec);return time_buffer;}void WriteLogToOneFile(const std::string &logname, const std::string &message){umask(0);int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) return;write(fd, message.c_str(), message.size());close(fd);}void WriteLogToClassFile(const std::string &levelstr, const std::string &message){std::string logname = logdir;logname += "/";logname += _filename;logname += levelstr;WriteLogToOneFile(logname, message);}void WriteLog(const std::string &levelstr, const std::string &message){switch(_style){case Screen:std::cout << message;break;case OneFile:WriteLogToClassFile("all", message);break;case ClassFile:WriteLogToClassFile(levelstr, message);break;default:break;}}void LogMessage(int level, const char* format, ...) // 类C的一个日志接口{char rightbuffer[1024];va_list args; // 这是一个char *类型(或者void *)的指针va_start(args, format); // 让arg指向可变部分vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 将可变部分按照指定格式写入到content中va_end(args); // 释放args, args = nullptrchar leftbuffer[1024];std::string levelstr = LevelToString(level);std::string currtime = TimeStampExLocalTime(); // 获取当前时间std::string idstr = std::to_string(getpid());snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ", \levelstr.c_str(), currtime.c_str(), idstr.c_str());std::string loginfo = leftbuffer;loginfo += rightbuffer;{LockGuard lockguard(&_mutex);WriteLog(levelstr, loginfo);}}~Log(){}
private:int _style;std::string _filename;pthread_mutex_t _mutex;
};// 配置log
Log lg;class Config
{
public:Config(){// 在此配置lg.Enable(Screen);}~Config(){}
};Config config;
5. LockGurad.hpp
#pragma once#include <pthread.h>// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:Mutex(pthread_mutex_t *lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t *_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;
};
5.3 服务器端设计
1. 服务器主程序——Main.cc
- 我们希望通过
./udp_server ip port
的方式启动服务器,ip和端口号需要我们手动传入。 UdpServer
就是一个服务器类,接下来就要逐步实现这个类。
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>// 使用手册
void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_ip local_port\n" << std::endl;
}// ./udp_server 127.0.0.1 8888
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);return Usage_Err;}std::string ip = argv[1]; // 拿到字符串风格的ip地址uint16_t port = std::stoi(argv[2]); // 拿到2字节整数风格的端口号std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(ip, port));usvr->Init(); // 初始化服务器usvr->Start(); // 启动服务器return 0;
}
- 接下来设计
UdpServer.hpp
。
2. 设计成员变量+搭建基本框架
- 理论来讲,需要三个成员变量:
- sockfd(这个大家可能会有疑问,不着急,在代码中学习);
- ip地址;
- port端口号。
- 设计一个
Init
接口,初始化服务器;再设置一个Start
接口,启动服务器。
const static std::string defaultip = "0.0.0.0"; // 默认ip地址
const static int defaultfd = -1;
const static uint16_t defaultport = 8888; // 默认端口class UdpServer : public nocopy
{
public:UdpServer(const std::string &ip = defaultip, uint16_t port = defaultport):_ip(ip), _port(port), _sockfd(defaultfd){}void Init(){...}void Start(){... }~UdpServer(){close(_sockfd);}
private:std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节
};
3. 设计Init接口
- 初始化服务器分为两步:
- a. 创建socket;
- b. 绑定,指定网络信息。
class UdpServer : public nocopy
{
public:...void Init(){// 1. 创建socket(本质就是创建了文件细节)_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp协议if (_sockfd < 0){ // 创建socket失败lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd); // 创建socket成功// 2. 绑定(指定网络信息)struct sockaddr_in local;bzero(&local, sizeof(local)); // 初始化local结构体local.sin_family = AF_INET; // 表明地址格式是IPV4 local.sin_port = htons(_port); // 端口号转网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列// 将网络信息设置进内核(网络信息和文件信息关联)int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if(n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}}...private:std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节
};
- 使用socket接口创建UDP套接字:
_sockfd = socket(AF_INET, SOCK_DGRAM, 0)
。domain
:指定协议族(Address Family),AF_INET
表示使用 IPv4 地址族。type
:指定套接字类型,SOCK_DGRAM
表示创建一个数据报套接字,适用于 UDP 协议。protocol
:指定使用的协议。对于 UDP,通常传入 0,表示使用默认协议(UDP)。
- 返回值是
int
类型,这个返回值的使用方式就像文件描述符一样,之后收发消息都要用到它。使得跟网络的交互,就像是在对某个文件进行写入和读取一样。
bind
绑定网络信息,需要我们提前填充好struct sockaddr
结构体,注意网络字节序和主机字节序之间的转化。注意和C++标准库中的std::bind
函数区分,他们是夫妻肺片和夫妻的区别,完全不是一个东西。
4. 设计Start接口
const static int defaultsize = 1024; // 默认收发消息时的缓冲区大小class UdpServer : public nocopy
{
public:...void Start(){char buffer[defaultsize];// 服务器永远不退出for(;;){struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 指明peer的长度,不能乱写// 收消息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n > 0){InetAddr addr(peer);buffer[n] = 0; // 最后一个字符串设置为'\0'std::cout << "[" << addr.PrintDebug() << "]# "<< buffer << std::endl;// 把消息返回sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}}...private:std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节
};
- 收消息:
recvfrom
是一个用于接收来自套接字的数据的系统调用,通常与基于数据报(UDP)的套接字一起使用。它可以从指定的套接字接收数据,并且可以获取发送方的地址信息。- sockfd:套接字描述符,由
socket()
系统调用返回。 - buf:指向接收数据的缓冲区。数据将被存储到这个缓冲区中。
- len:缓冲区的最大长度,即可以接收的最大字节数。
- flags:用于控制接收行为的标志。常见的标志包括:
- 0:默认行为,阻塞直到数据到达。
MSG_DONTWAIT
:非阻塞模式,如果无数据立即返回。
- src_addr:指向
sockaddr
结构的指针,用于存储发送方的地址信息(如 IP 地址和端口号)。如果不需要获取发送方地址,可以传入 NULL。 - addrlen:指向
socklen_t
类型的指针,用于指定src_addr
的大小。调用前应设置为src_addr
的大小,调用后将被设置为实际存储的地址长度。(不能乱填) - 返回值:
- 成功返回实际接收的数据长度;
- 失败返回-1。
- sockfd:套接字描述符,由
- 发消息:
sendto
是一个用于向指定地址发送数据的系统调用,通常与基于数据报(UDP)的套接字一起使用。- len:缓冲区数据长度;
- flag:用于控制发送行为的标志。常见的标志包括:
- 0:默认行为。
MSG_DONTWAIT
:非阻塞模式,如果无法立即发送数据,则返回 -1。
- dest_addr:指向目标地址的指针,通常是一个
struct sockaddr_in
(IPv4)或struct sockaddr_in6
(IPv6)结构。该结构包含目标的 IP 地址和端口号。 - addrlen:目标地址结构的大小(以字节为单位)。
- 返回值:
- 成功返回实际发送的字节数;
- 失败返回-1。
5. 完整的UdpServer.hpp头文件
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"const static std::string defaultip = "0.0.0.0"; // 默认ip地址
const static int defaultfd = -1;
const static uint16_t defaultport = 8888; // 默认端口
const static int defaultsize = 1024; // 默认收发消息时的缓冲区大小class UdpServer : public nocopy
{
public:UdpServer(const std::string &ip = defaultip, uint16_t port = defaultport):_ip(ip), _port(port), _sockfd(defaultfd){}void Init(){// 1. 创建socket(本质就是创建了文件细节)_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp协议if (_sockfd < 0){ // 创建socket失败lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd); // 创建socket成功// 2. 绑定(指定网络信息)struct sockaddr_in local;bzero(&local, sizeof(local)); // 初始化local结构体local.sin_family = AF_INET; local.sin_port = htons(_port); // 端口号转网络序列local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列// 将网络信息设置进内核(网络信息和文件信息关联)int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if(n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}}void Start(){char buffer[defaultsize];// 服务器永远不退出for(;;){struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 指明peer的长度,不能乱写// 收消息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n > 0){InetAddr addr(peer);buffer[n] = 0; // 最后一个字符串设置为'\0'std::cout << "[" << addr.PrintDebug() << "]# "<< buffer << std::endl;// 把消息返回sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){close(_sockfd);}
private:std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节
};
5.4 客户端设计
1. 客户端与服务器端的不同
- 无论是客户端还是服务器端,想要进行网络通信,
socket
是必须创建的,也是一定要bind
的。区别是,客户端不需要显示的bind
,而是在第一次发送消息时,随机绑定端口; - 这是为什么,客户端为什么不需要显示绑定,端口又为什么是随机的?
- 因为服务器端的
ip
和port
一定是众所周知,是服务方提供给大家的; - 而客户端上会有非常多的进程,也一定会有非常多的端口号。如果给客户端绑定一个固定端口,很容易发生端口冲突,所以干脆直接交给系统随机分配。
- 因为服务器端的
2. 完整代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>// 使用手册
void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}// ./udp_client server_ip server_port
int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);return 1;}std::string serverip = argv[1]; // 服务端ipuint16_t serverport = std::stoi(argv[2]); // 服务端port// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "socket error: " << strerror(errno) << std::endl;return 1;}std::cout << "create socket success: " << sockfd << std::endl;// 2. client要不要进行bind?一定要。但是不需要显示bind,client会在首次发送数据时自动进行bind// 为什么?server端的ip和端口号,一定是众所周知的,不可改变的,client需要bind随机端口// 为什么?因为client会非常多,不随机绑定端口容易出现端口冲突// 2.1 填充server信息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());while(true){// 我们要发的数据std::string inbuffer;std::cout << "Please Enter# ";std::getline(std::cin, inbuffer);// 给server端发消息ssize_t n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof(server));if(n > 0){char buffer[1024];// 收消息struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len); // 最后两个参数一般建议都是要填的(传空指针会有坑)if(m > 0){buffer[m] = 0;std::cout << "server echo# " << buffer << std::endl;}}}close(sockfd);return 0;
}
5.5 本地测试
1. 认识netstat命令
netstat
命令的-uanp
选项组合的含义如下:- -u:显示 UDP 相关的网络连接。
- -a:显示所有网络连接(包括监听和非监听的)。
- -n:以数字形式显示地址和端口号,而不是解析为域名或服务名称。
- -p:显示每个连接的进程信息(需要管理员权限)。
netstat -uanp
的输出通常包含以下字段:- Proto:协议类型(如 udp 或 udp6)。
- RefCnt:引用计数(通常为 1)。
- Local Address:本地地址和端口号(格式为
ip:port
)。 - Foreign Address:远端地址和端口号(格式为
ip:port
)。对于监听状态 的套接字,通常显示为*:port
。 - State:连接状态(对于 UDP,通常为空)。
- PID/Program name:占用该端口的进程 ID 和进程名称。
2. 运行服务器端
- 可以发现,本地的8888端口跑起了一个网络服务
./udp_server
。
3. 接下来我们启动客户端,并向服务器端发送数据
- 上图中,我们先启动了客户端,之后查看
netstat
,发现并没有属于客户端的网络服务启动(因为没有绑定);之后客户端向服务器端发送了一个数据,再次查看netstat
,发现有一个./udp_client
的客户端网络服务启动了,这也侧面验证了,客户端在第一次发送消息时bind
随机端口;之后我们又向服务端发送了一些消息,服务端也都收到了,并且把消息返回给了客户端,实验结束。 127.0.0.1
是回环地址,表示本主机ip,bind
该ip地址常用于,也只能用于做本地的网络cs测试。
5.6 跨网络测试
1. 问题引入
bind
环回地址,只能用于本地通信,我们想进行网络通信,怎么办?云服务器不是有公网ip吗,bind
试试:
- 发现绑定失败了。这是因为,我们所看到的云服务器的ip,是提供商虚拟出来的,无法绑定。但是如果你有一台真的Linux系统,比如说本机上安装的虚拟机,是可以绑定的。
- 但是,我们强烈不建议给服务器
bind
固定ip,因为一台服务器可能有多个网卡,也就有多个ip地址,如果给服务器bind
了固定ip,它将只能收到由那个ip对应的网卡传来的信息。所以在实际应用时,更推荐任意ip绑定的方式,只绑定固定端口。即无论客户端通过哪个ip地址访问到了服务器,只要端口号找对了,就可以和服务器进行网络通信。 - 基于上述内容,我们需要修改一下代码。
2. 修改服务器端代码
Main.cc
主程序:
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>// 使用手册
void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}// ./udp_server 8888
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);return Usage_Err;}// std::string ip = argv[1]; // 拿到字符串风格的ip地址uint16_t port = std::stoi(argv[1]); // 拿到2字节整数风格的端口号std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(port));usvr->Init(); // 初始化服务器usvr->Start(); // 启动服务器return 0;
}
UdpServer.hpp
头文件:bind
时ip绑定INADDR_ANY
。
// const static std::string defaultip = "0.0.0.0"; // 默认ip地址
const static int defaultfd = -1;
const static uint16_t defaultport = 8888; // 默认端口
const static int defaultsize = 1024; // 默认收发消息时的缓冲区大小class UdpServer : public nocopy
{
public:UdpServer(uint16_t port = defaultport):_port(port), _sockfd(defaultfd){}...void Init(){// 1. 创建socket(本质就是创建了文件细节)...// 2. 绑定(指定网络信息)struct sockaddr_in local;bzero(&local, sizeof(local)); // 初始化local结构体local.sin_family = AF_INET; local.sin_port = htons(_port); // 端口号转网络序列// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列local.sin_addr.s_addr = INADDR_ANY; // 绑定任意ip(这个宏实际上就是0)// 将网络信息设置进内核(网络信息和文件信息关联)int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));...}...private:// std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节
};
3. 给出一份Windows下的客户端代码,代码逻辑和之前写的Linux客户端一样,并且代码部分也大同小异
- 这里我选择的编译器是MinGW下的g++,编译的时候记得带
-lws2_32
选项。
#include <iostream>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <winsock2.h>
#include <Windows.h>#pragma comment(lib, "ws2_32.lib") // 类似于Linux中的-l选项,指明要链接的库// 下面写你的服务器公网ip和端口号
uint16_t serverport = 8888;
std::string serverip = "101.42.38.249"; int main()
{WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd); // 固定写法,初始化ws2库,版本是2.2struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socket error" << std::endl;return 1;}std::string message;char buffer[1024];while(true){std::cout << "Please Enter# ";std::getline(std::cin, message);sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}}// 区别2closesocket(sockfd);WSACleanup(); // 固定写法,清理ws2库return 0;
}
4. 开始测试
- 注意:云服务器上,大部分的端口都是不开放的,需要我们找到安全管理的相关部分,手动打开端口。
- 上图中,先启动服务器,然后查看服务器上的
netstat
,可以看到绑定的ip地址是0.0.0.0
,这个ip表示任意ip;右侧新出现的窗口是Windows上的cmd窗口,随后运行的a.exe
程序是Windows版的客户端,可以看到客户端发送的消息,服务器全部收到了,并且发回给了客服端,实验很成功。
5.7 UDP小应用——客户端输入命令,服务器端执行
只是让服务器把消息返回的话,未免太单调了,我们来写一个好玩的应用!
- 认识
popen
函数:- 该函数会创建一个子进程执行
command
对应的命令,并创建一个管道返回其文件描述符; type
指明管道方向,“r”:表示父进程可以从子进程的输出中读取数据(子进程的标准输出被重定向到管道);“w”:表示父进程可以向子进程的输入中写入数据(子进程的标准输入被重定向到管道)。
- 该函数会创建一个子进程执行
#include <stdio.h>FILE *popen(const char *command, const char *type);
- 服务器端主程序
Main.cc
:
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <string>
#include <vector>// 使用手册
void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}// 黑名单
std::vector<std::string> black_words = {"rm","unlink","cp","mv","chmod","exit","reboot","halt","shutdown","kill"
};// Debug
std::string OnMessadeDefault(std::string request)
{return request += "[got you!!!]";
}bool SafeCheck(std::string command)
{for(auto &k : black_words){std::size_t pos = command.find(k);if(pos != std::string::npos) return false;}return true;
}std::string ExecuteCommand(std::string command)
{if(!SafeCheck(command)) return "unsafe!!!";std::cout << "get a message: " << command << std::endl;FILE *fp = popen(command.c_str(), "r");if(fp == nullptr){return "execute error, reason is unknow";}std::string response;char buffer[1024];while(true){char *s = fgets(buffer, sizeof(buffer), fp);if(!s) break;else response += buffer;}pclose(fp);return response.empty() ? "success" : response;
}// ./udp_server 8888
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);return Usage_Err;}// std::string ip = argv[1]; // 拿到字符串风格的ip地址uint16_t port = std::stoi(argv[1]); // 拿到2字节整数风格的端口号// std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(OnMessadeDefault, port));std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(ExecuteCommand, port));usvr->Init(); // 初始化服务器usvr->Start(); // 启动服务器return 0;
}
- 改写
UdpSerever.hpp
:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <functional>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"// const static std::string defaultip = "0.0.0.0"; // 默认ip地址
const static int defaultfd = -1;
const static uint16_t defaultport = 8888; // 默认端口
const static int defaultsize = 1024; // 默认收发消息时的缓冲区大小using func_t = std::function<std::string(std::string)>; // 定义一个函数类型class UdpServer : public nocopy
{
public:UdpServer(func_t OnMesssade = nullptr, uint16_t port = defaultport):_port(port), _sockfd(defaultfd), _OnMessage(OnMesssade){}void Init(){// 1. 创建socket(本质就是创建了文件细节)_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp协议if (_sockfd < 0){ // 创建socket失败lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd); // 创建socket成功// 2. 绑定(指定网络信息)struct sockaddr_in local;bzero(&local, sizeof(local)); // 初始化local结构体local.sin_family = AF_INET; local.sin_port = htons(_port); // 端口号转网络序列// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列local.sin_addr.s_addr = INADDR_ANY; // 绑定任意ip(这个宏实际上就是0)// 将网络信息设置进内核(网络信息和文件信息关联)int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));if(n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}}void Start(){char buffer[defaultsize];// 服务器永远不退出for(;;){struct sockaddr_in peer; // 远端socklen_t len = sizeof(peer); // 指明peer的长度,不能乱写// 收消息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(n > 0){InetAddr addr(peer);buffer[n] = 0; // 最后一个字符串设置为'\0'// std::cout << "[" << addr.PrintDebug() << "]# "<< buffer << std::endl;// 处理消息std::string response = _OnMessage(buffer);// 把消息返回sendto(_sockfd, response.c_str(), response.size(), 0, (struct sockaddr*)&peer, len);}}}~UdpServer(){close(_sockfd);}
private:// std::string _ip; // 服务器ip地址uint16_t _port; // 端口号int _sockfd; // 文件细节func_t _OnMessage; // 回调方法
};
- 其他包括客户端在内的文件统统不用改,我们来做一下这个实验:
- 可以看到,输入的命令在远端执行了,并且过滤掉了我们设置好的,不安全的命令,实验成功。
6. 地址转换函数
1. 一些地址转换函数
-
本节只介绍基于IPv4的socket网络编程,
sockaddr_in
中的成员变量struct in_addr sin_addr
表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP地址。以下函数可以在字符串表示和in_addr
表示之间转换: -
字符串转
in_addr
函数:
-
in_addr
转字符串函数:inet_ntop
中的len参数是指缓冲区strptr
的大小。
-
其中
inet_pton
和inet_ntop
不仅可以转换IPv4的in_addr
,还可以转换IPv6的in6_addr
,因此函数接口是void *addrptr
。
2. 不安全的inet_ntoa
inet_ntoa
这个函数返回了一个char*
,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?
man
手册上说,inet_ntoa
函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。即使是这样,这个函数依然存在问题。- 对于返回值是
char*
类型的函数,我们就要尤其注意,因为它返回的是一个地址。如果有多个线程调用inet_ntoa
,很可能出现一种情况,接收值之间相互覆盖,混淆在一起。 - 在APUE中,明确提出
inet_ntoa
不是线程安全的函数。但是在Centos7上测试,并没有出现问题,可能内部的实现加了互斥锁。同学们课后自己写程序验证一下在自己的机器上inet_ntoa
是否会出现多线程的问题。 - 在多线程环境下,推荐使用
inet_ntop
,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题。
7. 实现一个简单的TCP网络程序
服务器端实现TCP通信一共分四步:
- 创建socket套接字;
bind
绑定;- 设置监听状态;
- 获取连接。
客户端分三步:
- 创建socket套接字;
bind
绑定;- 获取连接。
接下来,我会实现V1到V4,4个版本的通信程序,带领大家实现TCP通信!程序的功能还是,先从客户端收到信息,再返回给客户端。
7.1 V1版本——bug版
7.1.1 服务器端
1. 搭建服务器基本架构
- 下面注释中可以看到,需要我们实现的TCP建立连接的4个步骤;其中
Server
就是服务器要提供的服务,我们单独拎出来写。
class TcpServer : public nocopy
{
public:TcpServer(uint16_t port):_port(port), _isrunning(false){}void Init(){// 1. 创建套接字...// 2. 填充本地网络信息,并绑定...// 3. 设置socket为监听状态,TCP特有...}// 服务器要提供的服务void Service(int sockfd){...}void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接...// 5. 提供服务,v1~v4版本// v1Service(sockfd);}}~TcpServer(){}private:uint16_t _port;bool _isrunning; // 是否启动
};
2. 创建socket+bind
- 到这里为止,TCP建立连接的方式和UDP还一模一样;关于
setsockopt
的问题我们先不讲,只需要照着写即可。
const static int default_backlog = 5;class TcpServer : public nocopy
{
public:...void Init(){// 1. 创建套接字_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字if(_listensockfd < 0){lg.LogMessage(Fatal, "create socket errror, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensockfd);// 固定写法:解决一些少量绑定失败的问题 -- 后面讲到底层原理时再详细解释(这里要解决一个问题:服务端主动断开连接,再启动时bind失败的问题)int opt = 1;setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));// 2. 填充本地网络信息,并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local))){lg.LogMessage(Fatal, "bind error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bing socket success, sockfd: %d\n", _listensockfd);// 3. 设置socket为监听状态,TCP特有...}...private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
3. 设置监听状态
- 这一步是TCP特有的。
const static int default_backlog = 5;class TcpServer : public nocopy
{
public:...void Init(){...// 3. 设置socket为监听状态,TCP特有if(listen(_listensockfd, default_backlog)) // 第二个参数先不解释{lg.LogMessage(Fatal, "Listen socket error, errno code: %d, errror string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensockfd);}...~TcpServer(){close(_listensockfd);}private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
4. 获取连接,提供服务
accept
的第一个参数是刚刚socket
的返回值,后两个参数跟远端机器有关,接收远端机器的相关信息。accept
的返回值也是一个文件描述符,后面Service
中在进行网络通信时,使用的是accept
返回的fd
,而不是socket
返回的,这一点要尤其注意!!!
class TcpServer : public nocopy
{
public:...// TCP 连接可以进行全双工通信void Service(int sockfd){char buffer[1024];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0) // 网络中,read如果返回值是0,表示读到了文件结尾(对端关闭了链接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);if(sockfd < 0){lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);// 5. 提供服务,v1~v4版本// v1Service(sockfd);close(sockfd);}}...private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
- 该怎么理解呢?小剧场:
- 第一个
socket
返回的fd
,就像饭店门口招揽客人的人员(张三,这里指一类人),不断的把客人引到饭店里来;第二个accept
返回的fd
,就像饭店的后厨人员(李四),客人到饭店里以后,就不归张三管了,而是归李四管; - 张三招揽客人也可能失败,但是没关系,张三转头就去招揽别的客人了。对标到服务器中,设置
listen
状态就像饭店开始营业,accept
就像派张三出去揽客。招揽到客人后,就该给客人提供对应的服务啦。
- 第一个
5. TcpServer.hpp完整代码
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <wait.h>
#include <signal.h>
#include <unistd.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"const static int default_backlog = 5;class TcpServer : public nocopy
{
public:TcpServer(uint16_t port):_port(port), _isrunning(false){}void Init(){// 1. 创建套接字_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字if(_listensockfd < 0){lg.LogMessage(Fatal, "create socket errror, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensockfd);// 固定写法:解决一些少量绑定失败的问题 -- 后面讲到底层原理时再详细解释(这里要解决一个问题:服务端主动断开连接,再启动时bind失败的问题)int opt = 1;setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));// 2. 填充本地网络信息,并绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;if(bind(_listensockfd, (struct sockaddr*)&local, sizeof(local))){lg.LogMessage(Fatal, "bind error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bing socket success, sockfd: %d\n", _listensockfd);// 3. 设置socket为监听状态,TCP特有if(listen(_listensockfd, default_backlog)) // 第二个参数先不解释{lg.LogMessage(Fatal, "Listen socket error, errno code: %d, errror string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensockfd);}// TCP 连接可以进行全双工通信void Service(int sockfd){char buffer[1024];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0) // 网络中,read如果返回值是0,表示读到了文件结尾(对端关闭了链接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);if(sockfd < 0){lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);// 5. 提供服务,v1~v4版本// v1Service(sockfd);close(sockfd);}}~TcpServer(){close(_listensockfd);}private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
7.1.2 客户端
1. TcpClient.cc客户端
- 和UDP客户端相比,新增了断线重连功能。有些时候,可能因为一些网络原因导致连接建立失败,我们需要给客户端再一次重连的机会。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include <memory>
#include <cstdlib>// 默认尝试重连次数
#define Retry_Count 5 using namespace std; void Usage(const std::string& proc)
{std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}bool VisitServer(std::string &serverip, uint16_t serverport, int *pcnt)
{bool ret = true; // 返回值// 1. 创建socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return false;}// 2. connectstruct 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); // 更安全int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server)); // 自动进行bindif(n < 0){std::cerr << "connect error" << std::endl;ret = false;goto END;}*pcnt = 1; //重置cnt,注意重置的位置要在connect之后// 并没有像server一样,产生新的sockfd,未来我们就用connect成功的sockfd进行通信即可while(true){std::string inbuffer;std::cout << "Please Enter# ";getline(std::cin, inbuffer);if(inbuffer == "quit"){ret = true;goto END;}ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());if(n > 0){char buffer[1024];ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);if(m > 0){ buffer[m] = 0;std::cout << "get a echo message -> " << buffer << std::endl;}else{ret = false;goto END;}}else if(n == 0){// donothing}else {ret = false;goto END;}}END:close(sockfd);return ret;
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);return 1;}std::string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 连接服务器失败时,进行Retry_Count次重连int cnt = 1;while(cnt <= Retry_Count){bool result = VisitServer(serverip, serverport, &cnt); // 将cnt传入,方便重置if(result){break;}else{sleep(1);std::cout << "server offline, retrying..., count : " << cnt++ << std::endl;}}if(cnt >= Retry_Count){std::cout << "server offline, client quit..." << std::endl;}return 0;
}
2. 测试能否正常通信
- 先启动服务器端,然后查看
netstat
状态(-l
选项表示只查看监听状态的端口,-t
选项表示显示TCP连接),可以看到tcp_server
已经跑起来了;然后启动客户端,再查看网络状态(记得去掉-l
选项),可以发现客户端和服务器端都起来了;最后客户端发送消息,服务器端也收到并返回给了客户端,实验很成功。
3. 测试断线重连功能
- 结果符合预期:服务端先退出再启动,客户端先进入重连状态,服务器启动后客户端重连成功;第二次,服务器关闭,客户端尝试五次重连后仍未成功,超时未响应,客户端也关闭。
4. 有bug!
- 可以看到,只有一个客户端时,我们可以正常通信;当我们再启动一个客户端之后,发现后来的客户端发送的消息,无法到达服务器,直到第一个客户端关闭了,第二个客户端的信息才徐徐道来,这显然不是我们想看到的。
- 这是因为,目前我们的服务是一个单线程的,服务器再和一个客户端建立连接后,进入
Service
函数提供服务,无法再accept
新线程;直到旧的客户端主动关闭连接后,服务器才能accept
到新的客户。
7.2 V2版——多进程,wait版
1. 修改服务器端
class TcpServer : public nocopy
{
public:...// TCP 连接可以进行全双工通信void Service(int sockfd){...}void Start(){_isrunning = true;while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);if(sockfd < 0){lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);// 5. 提供服务,v1~v4版本// v2 多进程pid_t id = fork();if(id < 0){close(sockfd);continue;}else if(id == 0){// childclose(_listensockfd);if(fork() > 0) exit(0); // 子进程直接退出// 孙子进程正常处理(孙子进程被系统领养,资源交给系统回收)Service(sockfd);close(sockfd);exit(0);}else{close(sockfd);pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待(不建议改成非阻塞等待)if(rid == id){// do nothing}}}}...private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
- 首先思考,如果去掉
if(fork() > 0) exit(0)
这段代码行不行?肯定是不行的,因为这样就和V1没区别了,父进程还是会在waitpid
处阻塞等待,无法accept
。有同学就灵机一动,把waitpid
设置成非阻塞等待不就好了? - 我们不建议这么做,因为如果
waitpid
非阻塞等待,且没有子进程退出的情况下,每一次的执行waitpid
都是没有意义的,都是对资源的浪费。所以我们巧妙的写了if(fork() > 0) exit(0)
这段代码,子进程直接退出,父进程直接回收子进程,孙子进程提供服务。由于孙子进程的父进程死了,所以孙子进程被OS领养,变成孤儿进程,由系统释放资源。 - 还需要尤其注意文件描述符
fd
的释放:子进程会拷贝父进程的文件描述符表,这个表是浅拷贝,如果不即使释放不属于自己的文件描述符,文件描述符会越积越多,最终将表沾满,程序崩溃——这种现象也叫文件描述符泄漏。
2. 测试一波
- 完美!
- 有同学可能有疑问,为什么服务器端拿到了两个4号
sockfd
?要注意,这两个sockfd
指向的不是同一个文件,因为上一个子进程已经将不属于自己的fd
全close
了,所以在新的子进程的文件描述符表中,4号位置仍然是空的,申请的新fd
仍然指向自己文件描述符表的4号位置。
7.3 V3版——多进程信号版
1. 修改后的代码片
- 直接忽略
SIGCHLD
信号,Linux环境中,如果忽略该信号,子进程会自己释放资源。
class TcpServer : public nocopy
{
public:...void Start(){_isrunning = true;signal(SIGCHLD, SIG_IGN); // v3 在Linux环境中,如果对SIGCHLD进行忽略,子进程退出时,自动释放自己的资源while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);if(sockfd < 0){lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);// 5. 提供服务// v3 多进程 —— 信号版pid_t id = fork();if(id < 0){close(sockfd);continue;}else if(id == 0){// childclose(_listensockfd);Service(sockfd);close(sockfd);exit(0);}else{close(sockfd);} }}...private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
2. 测试:
7.4 V4——多线程版
1. 直接看代码
class TcpServer;class ThreadData
{
public:ThreadData(int sock, TcpServer *ptr):_sockfd(sock),_svr_ptr(ptr){}int Sockfd() { return _sockfd; }TcpServer *GetServer() { return _svr_ptr; }~ThreadData(){close(_sockfd);}private:int _sockfd;TcpServer *_svr_ptr;
};class TcpServer : public nocopy
{
public:...// TCP 连接可以进行全双工通信void Service(int sockfd){char buffer[1024];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0) // 网络中,read如果返回值是0,表示读到了文件结尾(对端关闭了链接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}static void *HandlerRequest(void *args){pthread_detach(pthread_self()); // 分离线程ThreadData *td = static_cast<ThreadData*>(args);td->GetServer()->Service(td->Sockfd());delete td;return nullptr;}void Start(){_isrunning = true;signal(SIGCHLD, SIG_IGN); // v3 在Linux环境中,如果对SIGCHLD进行忽略,子进程退出时,自动释放自己的资源while(_isrunning){// 4. 获取链接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);if(sockfd < 0){lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;}lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);// 5. 提供服务,v1~v4版本// v4 多线程ThreadData *td = new ThreadData(sockfd, this);pthread_t tid;// 主线程和新线程,不需要关闭所谓文件描述符,只需要将线程分离pthread_create(&tid, nullptr, HandlerRequest, td);}}~TcpServer(){close(_listensockfd);}private:uint16_t _port;int _listensockfd;bool _isrunning; // 是否启动
};
2. 测试
- 可以看到,文件描述符出现5了,因为大家都属于一个进程。
8. 如何理解:面向数据报&&面向字节流
我们在发送消息和接收消息时,似乎不用对消息做网络序列的转化。这是因为,IO类函数,
write/read
,recvfrom/sendto
,会自动帮我们做网络序列的转化。
由于我们还不了解协议的底层原理,所以今天理解起来仍然是很困难的,只能感性的认识一下。
数据和数据之间,是有边界的。对于UDP协议,我们不用关心如何确定数据之间的边界,因为UDP协议帮我们处理好了,用户直接拿到的,就是有边界的数据。
对于TCP协议,数据就像水流一样,没有边界,需要用户来确定数据的边界。
相关文章:
网络编程套接字【端口号/TCPUDP/网络字节序/socket编程接口/UDPTCP网络实验】
网络编程套接字 0. 前言1. 认识端口号2. 认识TCP和UDP协议3. 网络字节序4. socket编程接口5. 实现一个简单的UDP网络程序5.1 需求分析5.2 头文件准备5.3 服务器端设计5.4 客户端设计5.5 本地测试5.6 跨网络测试5.7 UDP小应用——客户端输入命令,服务器端执行 6. 地址…...
【C语言预编译处理精选题】
C语言预编译处理精选题 一、选择易错题1.1 纯文本替换,注意优先级!1.2 再来一道文本替换,别马虎1.3 宏定义的替换1.4带参数宏定义的空格问题1.5 " "的include1.6 条件编译1.7 预编译概念 二、填空易错题2.1 注意两个连续的 i2.2 异…...
云钥科技工业相机定制服务,助力企业实现智能智造
在工业自动化、智能制造和机器视觉快速发展的今天,工业相机作为核心感知设备,其性能直接决定了检测精度、生产效率和产品质量。然而,标准化工业相机往往难以满足复杂多样的应用场景需求,工业相机定制逐渐成为企业突破技术瓶颈…...
用了Cline和华为云的大模型,再也回不去了
这两年AI火热,受影响最大的还是程序员群体,因为编程语言是高度形式化的,完全可以用BNF等形式精确地定义,不像自然语言那样,容易出现歧义。另外开源是软件界的潮流,GitHub上有海量的开源代码可供AI来训练&am…...
vs2017版本与arcgis10.1的ArcObject SDK for .NET兼容配置终结解决方案
因电脑用的arcgis10.1,之前安装的vs2010正常能使用AO和AE,安装vs2017后无法使用了,在重新按照新版本arcgis engine或者arcObject费时费力,还需要重新查找资源。 用vs2017与arc10.1的集成主要两个问题,1:安装后vs中没有…...
Java对接微信支付全过程详解
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
微软 System Center Configuration Manager(SCCM)的组件文件
微软 System Center Configuration Manager(SCCM) 或 Microsoft Endpoint Configuration Manager(MECM) 的组件文件,属于企业级设备管理工具的一部分。以下是具体说明: C:\Windows\CCM\smsswd.exe C:\Windows\CCM\tsmanager.exe smsswd.exe 和 tsmanager.exe 是 Micros…...
C语言和C++到底有什么关系?
C 读作“C 加加”,是“C Plus Plus”的简称。 顾名思义,C 就是在 C 语言的基础上增加了新特性,玩出了新花样,所以才说“Plus”,就像 Win11 和 Win10、iPhone 15 和 iPhone 15 Pro 的关系。 C 语言是 1972 年由美国贝…...
10.PE导出表
一:定位导出表 PIMAGE_NT_HEADERS->OptionalHeader->DataDirectory[0] typedef struct _IMAGE_DATA_DIRECTORY {DWORD VirtualAddress; // 导出表的RVADWORD Size; // 导出表大小(没用) } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 该结构的Vi…...
springBoot中不添加依赖 , 手动生成一个token ,并校验token,在统一拦截器中进行校验 (使用简单 , 但是安全性会低一点)
要在 Spring Boot 里实现接口统一拦截并校验 Token,可以借助 Spring 的拦截器机制。下面是具体的实现步骤和代码示例。 1. 创建 Token 工具类 import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgori…...
VSCode C/C++ 环境搭建指南
一、前言 Visual Studio Code(简称 VSCode)是一款轻量级且功能强大的跨平台代码编辑器,凭借丰富的插件生态和高度的可定制性,深受开发者喜爱。对于 C/C 开发者而言,在 VSCode 中搭建开发环境,能够获得灵活…...
ES6(4) Map 集合详解
1. Map 集合简介 Map 是 ES6 提供的一种新的键值对数据结构,与普通对象(Object)不同,Map 的键可以是任意类型(包括对象、函数等)。 2. 创建 Map 集合 可以使用 new Map() 创建一个 Map,并在括…...
DeepSeek私有化部署与安装浏览器插件内网穿透远程访问实战
文章目录 前言1. 本地部署OllamaDeepSeek2. Page Assist浏览器插件安装与配置3. 简单使用演示4. 远程调用大模型5. 安装内网穿透6. 配置固定公网地址 前言 最近,国产AI大模型Deepseek成了网红爆款,大家纷纷想体验它的魅力。但随着热度的攀升,…...
【设计模式】建造者模式
三、建造者模式 3.3 建造者模式 建造者(Builder) 模式也称构建器模式、构建者模式或生成器模式,同工厂模式或原型 模式一样,也是一种创建型模式。建造者模式比较复杂,不太常用,但这并不表示不需要了 解和掌握该模式。建造者模式…...
一场由 ES 分片 routing 引发的问题
一场由 ES 分片 routing 引发的问题 ES 结构 {"poroperties": {"joinType": {"type": "join","eager_global_ordinals": true,"relations": {"spu": "sku"}},"id":{"type&q…...
搭建Python量化开发环境:从零开始的完整指南
搭建Python量化开发环境:从零开始的完整指南 在量化投资领域,一个稳定且高效的开发环境是成功的关键。本文将引导你一步步搭建起自己的Python量化开发环境,确保你能够顺利开始编写和运行量化策略。 🚀量化软件开通 Ὠ…...
JavaScript日期区间计算:精准解析年月日差异
一、应用场景与功能概述 在日常的制作项目或者是练习,我们经常需要计算两个日期之间的精确时间差。本文将通过一个JavaScript日期计算函数,详细解析如何实现精准的年/月/日差异计算,并探讨实际开发中的常见问题和解决方案。 二、核心功能解…...
大数据学习(71)-三范式构成
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言📝支持一…...
el-table 插槽踩过的坑 :slot-scope 和#default的区别
slot-scope和#default是Vue中用于定义插槽的两种不同语法,它们在Vue 2和Vue 3中有不同的应用场景和语法规则。 slot-scope 在Vue 2.x中,slot-scope是用于声明具名插槽并获取父组件传递过来的数据的主要方式。通过slot-scope可以定义一个变量scop…...
Linux一键安装node.js【脚本】
node.js一般不用系统的apt安装,而是用nvm这个前端的应用商店安装 node.js是js环境,nvm是安装nodejs管理器。npm是nodejs里的包管理器,安装模块的,类似于python的pip 把以下代码复制保存在一个文件里 比如nano install_nodejs.sh …...
vue3:pinia安装及其使用
一、安装 Pinia 的步骤 1、安装 Pinia npm install pinia 2、在 Vue 应用中引入 Pinia 在 main.js 中引入并注册 Pinia: import { createApp } from vue; import { createPinia } from pinia; import App from ./App.vue;const app createApp(App); app…...
vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性
native取消 在 Vue 3 中,v-on 的 .native 修饰符已经被移除。在 Vue 2 中,.native 修饰符用于在组件的根元素上监听原生 DOM 事件,但在 Vue 3 中,这一行为发生了变化。 在 Vue 3 中,所有未在子组件的 emits 选项中定…...
【漫话机器学习系列】146.Softmax 激活函数(Softmax Activation Function)
Softmax 激活函数详解 1. Softmax 函数概述 Softmax 函数(Softmax Activation Function)是一种常用于多分类任务的激活函数,广泛应用于机器学习和深度学习模型,特别是在神经网络的输出层。它的主要作用是将输入的多个实数值转换…...
解决:ModuleNotFoundError: No module named ‘_sqlite3‘
报错: from _sqlite3 import * ModuleNotFoundError: No module named _sqlite3安装sqlite3支持组件: sudo apt-get install libsqlite3-dev进入之前下载的python包下,重新编译和安装Python ./configure --enable-loadable-sqlite-extensions make &a…...
C++差分风暴:区间修改终极模板
目录 🔥 差分核心价值 🌟 一维差分模板 1. 核心思想 2. 代码实现 3. 动态图示 📦 二维差分模板 1. 核心公式 2. 代码实现 3. 二维修改示意图 🚨 六大避坑指南 💡 复杂度对比 🌈 LeetCode实战 &…...
easypoi导入Excel兼容日期和字符串格式的日期和时间
问题场景 在使用easypoi导入Excel时,涉及到的常用日期会有yyyy-MM-dd HH:mm:ss、yyyy-MM-dd和HH:mm:ss,但是Excel上面的格式可不止这些,用户总会输入一些其他格式,如 如果在定义verify时用下面这种格式定义,那么总会…...
《保险科技》
自己在保险行业工作很多年,只是接触了一些数据的内容,对于保险业务的知识了解的很少,想通过这本书补充一下,但是发现这本书就是一些知识的拼接。 先将保险的历史,后讲保险的定义,然后就是吹嘘保险行业和互联…...
QT编程之HTTP服务端与客户端技术
一、HTTP 服务器实现方案 QtWebApp 集成 将QtWebApp源码的 httpserver 目录导入项目,并在 .pro 文件中添加 include ($$PWD/httpserver/httpserver.pri)。配置 WebApp.ini 文件定义服务参数(IP、端口、线程池等),通过 HttpL…...
每日一题--计算机网络
一、基础概念类问题 1. TCP 和 UDP 的区别是什么? 回答示例: TCP:面向连接、可靠传输(通过三次握手建立连接,丢包重传)、保证数据顺序(如文件传输、网页访问)。 UDP:无…...
IIS 服务器日志和性能监控
Internet Information Services (IIS) 是 Microsoft 提供的一款功能强大、灵活且可扩展的 Web 服务器,用于托管网站、服务和应用程序。IIS 支持 HTTP、HTTPS、FTP、SMTP 和更多用于提供网页的协议,因此广泛用于企业环境。 IIS 的…...
Unity学习之Shader总结(一)
一、Lesson1 1、渲染流水线 (1)应用阶段 模型->模型处理(应用阶段)–>输入结构 应用阶段主要操作:粗粒度剔除、进行渲染设置、准备基本数据、输出到几何阶段 (2)几何阶段 输入结构-&…...
java,poi,提取ppt文件中的文字内容
注意,不涉及图片处理。 先上pom依赖: <!-- 处理PPTX文件 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency><!--…...
matlab R2024b下载教程及安装教程(附安装包)
文章目录 前言一、matlab R2024b 安装包下载二、matlab R2024b安装教程 前言 为帮助大家顺利安装该版本软件,特准备matlab R2024b下载教程及安装教程,它将以简洁明了的步骤,指导你轻松完成安装,开启 MATLAB R2024 的强大功能之旅…...
设计模式之外观模式:原理、实现与应用
引言 外观模式(Facade Pattern)是一种结构型设计模式,它通过提供一个统一的接口来简化复杂系统的使用。外观模式隐藏了系统的复杂性,使得客户端可以通过一个简单的接口与系统交互。本文将深入探讨外观模式的原理、实现方式以及实…...
移远QuecPython模组中的看门狗技术:如何提升设备可靠性
概述 对蜂窝通信模组而言,看门狗(Watchdog)是一种硬件或软件的监控机制,用于监测模组的运行状态。当模组因为外界干扰或程序错误陷入死循环时,看门狗会自动触发模组重启,从而恢复模组的运行状态。 对看门…...
汽车感性负载-智能高边钳位能量计算
随着汽车电子技术的发展,新的电子电气架构下,越来越多的执行部件在车身出现,比如电磁阀、风机、水泵、油泵、雨刮继电器等常用的执行器, 它们一般都表现为感性特点。驱动这些负载的最简单和最常见的方法是将它们连接到高边侧开关(…...
Kubernetes之ETCD
ETCD 是 Kubernetes 的核心组件之一,它是一个分布式键值对存储数据库,专为高可用性和一致性设计。它在 Kubernetes 中的主要作用是存储所有集群数据,包括配置数据和状态信息。ETCD 是 Kubernetes 的“大脑”,其稳定性对整个集群至…...
通过Geopandas进行地理空间数据可视化
目录 引言 安装与导入 数据加载与探索 数据预处理 基本地图可视化 添加其他数据到地图上 空间分析与查询 地图叠加与分组 空间缓冲区 交互式地图可视化 实际应用案例 城市规划 环境监测 结论 引言 在数据科学领域,地理空间数据可视化扮演着至关重要的角色。它不…...
堆(heap)
堆?对于初学者来说,或许是一个陌生十足的概念。 但!堆不可怕。 什么是堆? 学术上,常常是这样说的(一个完全二叉树)。 没毛病,要想更好的理解堆(heap),确实需要好好掌…...
Leetcode-回溯-组合型
22. 括号生成 - 力扣(LeetCode) 这题并没有才有恢复现场的做法 而是直接覆盖 题目核心是看穿本质 在代码执行过程中左括号必须大于等于右括号 以及回溯最底层的递归条件是递归长度已经达到2n 还有做括号要小于n 又因为i左右 因此右等于i-左 ope…...
Modbus通信协议基础知识总结
1. 数据类型与存储区分类 Modbus协议将数据分为四类存储区,通过存储区代号区分: 输出线圈(0x):可读写,对应二进制开关量(如继电器状态),地址范围000001-065536ÿ…...
python pip 最最开始新手教程/pip安装jupyter
pip 安装 直接安排了python即安装了pip,一般找pip的是安装了python之后用不了的,出现问题的,我也是因为这个所以写了这篇文章。 一、找不到pip,在C盘的cmd 输入pip -- version显示没有的是因为pip不能直接使用,需要额外配置环境变量 在系统…...
MATLAB 控制系统设计与仿真 - 28
MATLAB状态空间控制系统分析 - 极点配置 就受控系统的控制律的设计而言,由状态反馈极点配置和输出反馈极点配置。 状态反馈极点配置问题就是:通过状态反馈矩阵K的选取,使闭环系统的极点,即(A-BK)的特征值恰好处于所希望的一组给定闭环极点的位置。 另外,线性定常系统可…...
[leetcode] 面试经典 150 题——篇3:滑动窗口
[leetcode] 面试经典 150 题——篇3:滑动窗口 方法概述基本原理适用场景示例说明 1. [中等] 长度最小的子数组(leetcode 209题)题目描述解题思路python代码 2. [中等] 无重复字符的最长子串(leetcode 5题)题目描述解题思路python代码 方法概述 滑动窗口是一种常用的…...
华为云虚拟化技术
华为云底层的虚拟化技术是一种将物理资源(如服务器、存储设备和网络)抽象成虚拟资源的技术。通过这种技术,用户可以将物理资源划分为多个虚拟资源,从而提高资源利用率和灵活性1。 华为云底层虚拟化的定义和作用 华为云底层…...
在Pycharm配置conda虚拟环境的Python解释器
〇、前言 今天在配置python解释器时遇到了这样的问题 经过一下午自行摸索、上网搜寻后,终于找到的解决的方案,遂将该方法简要的记录下来,以备后用,并希望能帮助到有同样问题或需求的朋友:) 我所使用的软件的版本如下,假…...
【AI】在AWS AI芯片服务上部署运行Qwen 2.5模型
推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 准备选项 1:在 Amazon EC2 Inf2 上部署 TGI选项 2:在 SageMaker 上部署 TGI清理Qwen 2.5 多语言大型语言模型(LLMs) 是一系列预先…...
如何用日事清做研发目标、需求、规划、迭代、Bug、效能、复盘、绩效一站式管理
近年来,受监管环境趋严、盈利模式转型、市场竞争加剧等影响,互联网行业逐步进入深度变革与持续创新阶段。在此过程中,项目进度延期、需求频繁变更、开发流程混乱等痛点问题频发,导致部分互联网企业创新升级的步伐停滞不前。 企业…...
手搓智能音箱——语音识别及调用大模型回应
一、代码概述 此 Python 代码实现了一个语音交互系统,主要功能为监听唤醒词,在唤醒后接收用户语音问题,利用百度语音识别将语音转换为文本,再调用 DeepSeek API 获取智能回复,最后使用文本转语音功能将回复朗读出来。 …...
Docker安装mysql——Linux系统
拉取mysql镜像 docker pull mysql 查看镜像 docker images 运行镜像(这一步的作用:数据持久化,通过挂载卷将日志、数据和配置文件存储在主机上,避免容器删除导致数据丢失) docker run -p 3306:3306 --name mysql …...