Linux知识点 -- 网络编程套接字
Linux知识点 – 网络编程套接字
文章目录
- Linux知识点 -- 网络编程套接字
- 一、预备知识
- 1.认识端口号
- 2.套接字
- 3.TCP协议与UDP协议
- 4.网络字节序
- 二、socket编程接口
- 1.socket常见API
- 2.sockaddr结构
- 三、UDP套接字编程
- 1.直接打印客户端信息
- 2.执行客户端发来的指令
- 3.多用户聊天
- 4.在windows环境下运行客户端,与云服务器下的Linux服务端通信
- 四、TCP套接字
- 1.打印客户端信息并发回
- 2.多线程版服务器
- 五、关于地址转换函数
- 1.字符串转in_addr的函数
- 2.in_addr转字符串的函数
- 六、TCP协议通讯流程
一、预备知识
1.认识端口号
端口号(port)是传输层协议的内容;
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
- IP地址+端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用;
- 源端口号就是发送数据的端口,目的端口号就是接收数据的端口;
注:一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定;
在平常使用的APP上,客户端软件发出的请求,通过网络传输到服务端软件进行处理;
真正的网络间通信,本质其实是进程间通信;
将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程;
2.套接字
IP地址 + 端口号就是套接字;
3.TCP协议与UDP协议
TCP协议:
- 传输层协议
- 有连接(可理解为打电话,对方必须响应)
- 可靠传输(为保证可靠性,需要更多的策略)
- 面向字节流
UDP协议:
- 传输层协议
- 无连接(写信或发邮件)
- 不可靠传输(使用成本更低;适合直播、视频网站)
- 面向数据报
4.网络字节序
内存中的数据存储分大端和小端,因此网络通信中不同的主机也会有不同的大小端主机之间通信;
网络规定:所有网络数据,都必须是大端;
网络字节序和主机字节序的转换库函数:
其中:
- h表示host,n表示network,I表示32位长整数,s表示16位短整数;
- 例如htonI表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回;
二、socket编程接口
1.socket常见API
2.sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket;然而,各种网络协议的地址格式并不相同;
常见的套接字类型:
- 域间socket
- 原始socket
- 网络socket
理论上是三种场景,对应三套接口;
但实际上所有的地址接口都是统一的;
所有接口传入的都是struct sockaddr这个类型的地址参数;
网络和域间套接字的前两个字节是不同的,在函数内部会进行判断;
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址;
- IPv4、IPv6地址类型分别定义为常数AF_ INET、AF_INET6;这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容;
- socket API可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性,可以接收IPv4, IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
三、UDP套接字编程
1.直接打印客户端信息
udp_server.hpp
-
socket接口:创建套接字
创建套接字,建立一个通信的一端
创建成功,返回文件描述符 ,但是UDP是非字节流的操作,不能使用之前的文件接口访问;失败返回-1,并设置errno;
domain:域,套接字的类型;
AF_INET是网络通信
AF_LOCAL是本地通信
type:套接字通信种类,UDP是面向数据报,使用SOCK_DGRAM;
SOCK_DGRAM:用户数据报
protocol:前两个参数确定,这个参数也确定了,一般写为0; -
bind接口:绑定套接字
将用户设置的ip和port在内核中和我们当前的进程强关联;
成功返回0,失败返回-1,设置errno;
sockfd:套接字;
sockaddr:通用addr结构,AF_INET网络通信使用sockaddr_in地址结构;
要显示出来sockaddr_in类型,需包含头文件;
sockaddr_in结构体:
其中,sin_port为端口号,sin_addr为ip地址,还需要设置一个sin_family成员,与套接字的类型一致; -
IP地址格式转换
“192.168.110.132” -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域;
理论上,表示一个IP地址,其实4字节就够了;
需要将点分十进制字符串风格的IP地址 <-> 4字节;
Sin_addr其实是一个整数类型;
bzero接口:将指定长度的空间全部置0;
先要将点分十进制字符串风格的IP地址 -> 4字节,再将4字节主机序列 -> 网络序列;
有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据;
其中,inet_addr接口就是将字符串IP地址转换为网络序列IP地址; -
recvfrom:从网络中读取数据
buf:数据存储缓冲区
len:缓冲区大小
flags:读取方式,默认0为阻塞方式
src_addr:输出型参数,拿到数据发送方的ip和port
addrlen:输入输出型参数;输入: src_addr 缓冲区大小;输出: 实际读到的src_addr大小 -
网络IP转主机IP
-
sendto:向目标主机发送数据
dest_addr:目的主机地址
addrlen:dest_addr的大小 -
本地环回:127.0.0.1
client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中,用于本地服务器的测试; -
服务器IP
云服务器无法bind公网IP,对于服务器来讲,也不建议绑定确定的IP;
INADDR_ANY宏默认为0,是让服务器在工作过程中,可以获取任意IP的数据;
只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
服务端只需要指定端口号;
#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_#include"log.hpp"
#include<iostream>
#include<unordered_map>
#include<cstdio>
#include<string>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<strings.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<queue>#define SIZE 1024class UdpServer
{
public:UdpServer(uint16_t port, std::string ip = ""): _port(port), _ip(ip), _sock(-1) // 套接字先初始化为-1{}bool initServer(){// 1.创建套接字_sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_NET与PF_NET是一样的if(_sock < 0){logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(2);}//2.bind:将用户设置的ip和port在内核中和我们当前的进程强关联struct sockaddr_in local; // 本地主机地址bzero(&local, sizeof(local));local.sin_family = AF_INET; // 与套接字类型一致// 服务器的IP和端口号未来也是要发送给对方主机的,要先将数据发送到网络local.sin_port = htons(_port); // 考虑大小端转换// 将主机IP转换成网络IPlocal.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)// 绑定,须强转成同一类型struct sockaddr*{logMessage(FATAL, "%d:%s", errno, strerror(errno));exit(3);}logMessage(NORMAL, "init udp server done ... %s", strerror(errno));return true;}//直接显示客户端发的消息void start(){// 服务器是永不退出的char buffer[SIZE];for(;;){struct sockaddr_in peer; // 远端地址,输出型参数bzero(&peer, sizeof(peer));socklen_t len = sizeof(peer); // 输入输出型参数// 输入时,大小为src_addr的大小// 输出时,值为实际读到的dst_addr大小// 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);if(s > 0){buffer[s] = 0; //目前的数据当作字符串uint16_t cli_port = ntohs(peer.sin_port); //输出型参数,从网络中来的std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络4字节IP转为主机IPprintf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);}//写回数据sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);}}~UdpServer(){if(_sock >= 0){close(_sock); // 析构关闭套接字}}private://一个服务器,一般需要ip地址和port(16位整数)uint16_t _port;std::string _ip;int _sock; // 套接字
};#endif
udp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>static 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[0]);exit(1);}//创建套接字int sock = socket(AF_INET, SOCK_DGRAM, 0);if(sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind// client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->// client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??// client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)std::string message;struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(atoi(argv[2]));server.sin_addr.s_addr = inet_addr(argv[1]);char buffer[1024];while(true){std::cout << "请输入你的信息# ";std::getline(std::cin, message);if(message == "quit") break;//发送消息//当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和portsendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));struct sockaddr_in temp;socklen_t len = sizeof(temp);//接受回信ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << "server echo# " << buffer << std::endl;}} close(sock);return 0;
}
- client不需要bind特定的套接字
client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢;
client一般不需要显示的bind指定port,而是让OS自动随机选择;
当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和port;
udp_server.cc
#include"udp_server.hpp"
#include<memory>
#include<cstdlib>static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << "port\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port)); // 使用智能指针管理对象svr->initServer();svr->start();return 0;
}
- 服务器不需要指定客户的IP地址获取数据;
让服务器再工作过程中,可以获取任意IP的数据;
只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
服务端只需要指定端口号;
Log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *gLevelMap[] = {"DEBUG","NORMAL","WARNING","ERROR","FATAL"
};#define LOGFILE "./threadpool.log"// 完整的日志功能,至少需要:日志等级 时间 支持用户自定义(日志内容,文件行,文件名)void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOWif (level == DEBUG)return;
#endifchar stdBuffer[1024]; // 标准部分time_t timestamp = time(nullptr);snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);char logBuffer[1024]; // 自定义部分va_list args;va_start(args, format);vsnprintf(logBuffer, sizeof(logBuffer), format, args);va_end(args);// FILE *fp = fopen(LOGFILE, "a");// fprintf(fp, "%s %s\n", stdBuffer, logBuffer);// fclose(fp);printf("%s %s\n", stdBuffer, logBuffer);
}
上面的代码实现了服务器直接打印客户端发送的信息,并将信息在发回客户端;
- netstat -anup指令:查看当前网络中的UDP协议服务器状态
运行结果:
2.执行客户端发来的指令
popen接口可以执行传入的字符串command;
执行command,创建pipe管道进行进程间通信,fork子进程执行(exec)command命令;
FILE可以将执行成果通过FILE指针进行读取;*
udp_server.hpp中的start成员函数
// 执行客户端指令void start(){// 服务器是永不退出的char buffer[SIZE];for (;;){struct sockaddr_in peer; // 远端地址,输出型参数bzero(&peer, sizeof(peer));socklen_t len = sizeof(peer); // 输入输出型参数// 输入时,大小为src_addr的大小// 输出时,值为实际读到的dst_addr大小char result[256];std::string cmd_echo;// 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){buffer[s] = 0; // 目前的数据当作字符串if (strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir")){std::string err_message = "bad !";std::cout << err_message << buffer << std::endl;sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);continue;}FILE* fp = popen(buffer, "r");// 执行指令if(nullptr == fp){logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));continue;}while(fgets(result, sizeof(result), fp) != nullptr) //通过fp读取执行结果{cmd_echo += result;}fclose(fp);}// 写回数据sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);}}
运行结果:
3.多用户聊天
服务器将返回的消息发送给所有用户,聊天室功能;
udp_server.hpp中的start函数
class UdpServer
{
public:void start(){// 服务器是永不退出的char buffer[SIZE];for (;;){struct sockaddr_in peer; // 远端地址,输出型参数bzero(&peer, sizeof(peer));socklen_t len = sizeof(peer); // 输入输出型参数// 输入时,大小为src_addr的大小// 输出时,值为实际读到的dst_addr大小char key[64]; // 保存客户端地址信息// 读取数据ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){buffer[s] = 0; // 目前的数据当作字符串uint16_t cli_port = ntohs(peer.sin_port); // 获取端口号std::string cli_ip = inet_ntoa(peer.sin_addr); // 获取4字节ip地址snprintf(key, sizeof key, "%s-%u", cli_ip.c_str(), cli_port); //格式化打印到key中logMessage(NORMAL, "key: %s", key);auto it = _users.find(key);if(it == _users.end()) // 若用户不存在,则添加用户{logMessage(NORMAL, "add new user : %s", key);_users.insert({key, peer});}}for(auto &iter : _users) // 将消息推送给所有用户{std::string sendMessage = key;sendMessage += "# ";sendMessage += buffer; // 发回消息的时候加上用户信息logMessage(NORMAL, "push message to %s", iter.first.c_str());sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));}}}private:// 一个服务器,一般需要ip地址和port(16位整数)uint16_t _port;std::string _ip;int _sock; // 套接字std::unordered_map<std::string, struct sockaddr_in> _users; // 存储用户信息};
客户端一边收消息,一边发消息,多线程;
thread.hpp
#pragma once#include <iostream>
#include <string>
#include <functional>
#include <cstdio>typedef void *(*fun_t)(void *); // 定义函数指针类型,后面回调class ThreadData // 线程信息结构体
{
public:void *_args;std::string _name;
};class Thread
{
public:Thread(int num, fun_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); // 直接将_tdata作为参数传给回调函数}void join() // 线程等待{pthread_join(_tid, nullptr);}std::string name(){return _name;}~Thread(){}private:std::string _name;fun_t _func;ThreadData _tdata;pthread_t _tid;
};
udp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"uint16_t server_port = 0;
std::string server_ip;static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << "serverIP serverPort\n"<< std::endl;
}static void *udpSend(void *args)
{int sock = *(int *)((ThreadData *)args)->_args;std::string name = ((ThreadData *)args)->_name;std::string message;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());while (true){std::cerr << "请输入你的信息# "; // 标准错误,fd == 2打印std::getline(std::cin, message);if (message == "quit"){break;}sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));}return nullptr;
}static void *udpRecv(void *args)
{int sock = *(int *)((ThreadData *)args)->_args;std::string name = ((ThreadData *)args)->_name;char buffer[1024];while(true){memset(buffer, 0, sizeof buffer);struct sockaddr_in temp;socklen_t len = sizeof temp;ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}
}int main(int argc, char *argv[])
{if (argc != 3){usage(argv[0]);exit(1);}// 创建套接字int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}server_ip = argv[1];server_port = atoi(argv[2]);// client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind// client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->// client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??// client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送消息线程std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收消息线程sender->start();recver->start();sender->join();recver->join();close(sock);return 0;
}
运行结果:
通过创建管道文件,让客户端发送信息和接收信息的会话分开:
4.在windows环境下运行客户端,与云服务器下的Linux服务端通信
windows下的套接字编程与Linux下的大致接口相同,只需要添加一些windows下独有的语句就可以:
udpClient.cpp
#pragma warning(disable:4996) //禁用报错
#include <WinSock2.h> //win套接字头文件
#include <iostream>
#include <string>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //固定用法,引入win下的套接字库
uint16_t serverport = 8080;
std::string serverip = "120.78.126.148"; //云服务器公网ip
int main()
{// windows 独有的WSADATA WSAData;WORD sockVersion = MAKEWORD(2, 2); //选中库if (WSAStartup(sockVersion, &WSAData) != 0)return 0;SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);if (INVALID_SOCKET == clientSocket){cout << "socket error!";return 0;}sockaddr_in dstAddr;dstAddr.sin_family = AF_INET;dstAddr.sin_port = htons(serverport);dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());char buffer[1024];while (true){std::string message;std::cout << "请输入# ";std::getline(std::cin, message);sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);if (s > 0){buffer[s] = '\0';std::cout << "server echo# " << buffer << std::endl;}}// windows 独有closesocket(clientSocket);WSACleanup();return 0;
}
运行结果:
四、TCP套接字
1.打印客户端信息并发回
(1)tcp_server.hpp
-
套接字类型
由于TCP协议是面向字节流的协议,因此套接字的类型需选择SOCK_STREAM;
-
面向连接的协议
因为TCP协议是面向连接的,当我们正式通信的时候,需要先建立连接;
将套接字状态设置为监听状态;
backlog:全链接队列长度;
成功返回0,失败返回-1,并设置errno; -
netstat -antp指令:查看网络中的TCP协议服务器
-
获取连接
accept接口:获取与客户端的连接;
addr:输出型参数,拿到客户端的ip和端口号;
addlen:输入输出型参数,输入服务器addr大小,输出客户端addr大小;
返回值:成功:返回套接字,这是真正进行IO服务的套接字;失败返回-1;
传入的sockfd参数那个套接字只是获取底层的连接,是监听套接字; -
读取消息
TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取);
单进程循环处理
一次处理一个客户端,处理完了一个,才能处理下一个;
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"// 打印服务
static void service(int sock, const std::string &cli_ip, const uint16_t &cli_port)
{// 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)char buffer[1024];while (true){ssize_t s = read(sock, buffer, sizeof(buffer - 1));if (s > 0){buffer[s] = 0;std::cout << cli_ip << ":" << cli_port << "# " << buffer << std::endl;}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);break;}else{logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}
}class TcpServer
{
private:const static int gbacklog = 20;public:TcpServer(uint16_t port, std::string ip = ""): _port(port), _ip(ip){}void initServer(){// 1.创建套接字socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", _listensock);// 2.bind -- 文件 + 网络struct 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(3);}// 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接if (listen(_listensock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}void start(){while (true){// 4.获取连接struct sockaddr_in src; // 输出型参数,获取对方主机地址socklen_t len = sizeof src; // 输入输出型参数,对方主机地址的长度int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue; // 获取连接失败后继续获取}// 获取连接成功了,通信对象的地址在accept函数的后两个参数中uint16_t cli_port = ntohs(src.sin_port);std::string cli_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",servicesock, cli_ip.c_str(), cli_port);// 开始进行通信服务// 单进程循环版 -- 一次处理一个客户端,处理完一个才能处理下一个service(servicesock, cli_ip, cli_port);close(servicesock);}}~TcpServer(){}private:uint16_t _port;std::string _ip;int _listensock; // 监听套接字
};
(2)tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{if(argc != 2){usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(port));svr->initServer();svr->start();return 0;
}
- 使用远程登陆工具telnet模拟客户端
连接服务端
按照提示按下ctrl + ]进入telnet
运行结果
如果创建两个客户端
服务端只会响应第一个客户端,只有当第一个客户端退出时,第二个客户端才会被响应;
(3)多进程版服务器
tcp_server.hpp中的start成员函数
**父进程waitpid会阻塞进程,和单进程就没区别了;
- 方案一:
可以主动忽略SIGCHID信号让子进程自动释放僵尸状态;
父进程关闭servicesock,因为每个进程可用的文件描述符都是有限的,这里关闭了,还有子进程的fd指向文件,因此不会有问题;
如果不关闭,就会导致文件描述符泄露;
void start(){signal(SIGCHLD, SIG_IGN); // 对于SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态while (true){// 4.获取连接struct sockaddr_in src; // 输出型参数,获取对方主机地址socklen_t len = sizeof src; // 输入输出型参数,对方主机地址的长度int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue; // 获取连接失败后继续获取}// 获取连接成功了,通信对象的地址在accept函数的后两个参数中uint16_t cli_port = ntohs(src.sin_port);std::string cli_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",servicesock, cli_ip.c_str(), cli_port);// 开始进行通信服务//多进程版 -- 创建子进程//让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的pid_t id = fork();assert(id != -1);if(id == 0){//子进程,能够继承父进程的文件fd//子进程是来提供服务的,不需要知道监听socketclose(_listensock);//关闭不需要的文件描述符service(servicesock, cli_ip, cli_port);exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态}close(servicesock);}}
运行结果:
可以进行多客户端通信了;
- 方案二(不忽略SIGCHLD信号)
在子进程中再fork,创建孙子进程,让孙子进程执行service,子进程立马退出;
孙子进程就变成孤儿进程,让OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程;
void start(){while (true){// 4.获取连接struct sockaddr_in src; // 输出型参数,获取对方主机地址socklen_t len = sizeof src; // 输入输出型参数,对方主机地址的长度int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue; // 获取连接失败后继续获取}// 获取连接成功了,通信对象的地址在accept函数的后两个参数中uint16_t cli_port = ntohs(src.sin_port);std::string cli_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",servicesock, cli_ip.c_str(), cli_port);// 多进程版 -- 创建子进程// 让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的pid_t id = fork();assert(id != -1);// 多进程版 -- 创建孙子进程if (id == 0){// 子进程,能够继承父进程的文件fd// 子进程是来提供服务的,不需要知道监听socketclose(_listensock); // 关闭不需要的文件描述符if(fork() > 0){exit(0);// 子进程调用fork,创建孙子进程,然后子进程退出}// 孙子进程在子进程退出后编程孤儿进程,OS领养,OS在孤儿进程退出后自动回收service(servicesock, cli_ip, cli_port);exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态}waitpid(id, nullptr, 0); // 子进程及时退出,不会阻塞等待close(servicesock);}}
运行结果:
(4)客户端:
TCP的服务端需要bind,一定需要一个确定的port;
TCP的客户端不需要bind,一旦bind,就说明客户端绑定的是一个具体端口号,两个客户端可能是由不同公司写的,可能端口号会出现冲突;
需要让操作系统自动进行port选择;
客户端需要连接别人;
-
connect接口:连接特定的IP和port
后两个参数与sendto参数相同,目的主机的地址和地址的大小;
成功返回0,失败返回-1; -
send接口:tcp发送接口
flags默认为0 -
recv接口:tcp接收接口
注:send和recv也可以使用read和write代替;
tcp_client.cc
#include "tcp_server.hpp"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>static 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[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){if (!alive) // 如果连接不存在,重新建立连接{sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){std::cerr << "socket error" << std::endl;exit(2);}// client不需要显式bind,但是一定需要port// 要让OS自动进行port选择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);}std::cout << "connect success" << std::endl;alive = true;}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 l = 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;
}
运行结果:
2.多线程版服务器
(1)每次连接时都创建新的线程
tcp_server.hpp
class ThreadData
{
public:int _sock;std::string _ip;uint16_t _port;
};class TcpServer
{
private:const static int gbacklog = 20;static void *threadRoutine(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){}void initServer(){// 1.创建套接字socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", _listensock);// 2.bind -- 文件 + 网络struct 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(3);}// 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接if (listen(_listensock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 多线程版void start(){while (true){// 4.获取连接struct sockaddr_in src; // 输出型参数,获取对方主机地址socklen_t len = sizeof src; // 输入输出型参数,对方主机地址的长度int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue; // 获取连接失败后继续获取}// 获取连接成功了,通信对象的地址在accept函数的后两个参数中uint16_t cli_port = ntohs(src.sin_port);std::string cli_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",servicesock, cli_ip.c_str(), cli_port);// 多线程ThreadData* td = new ThreadData();td->_sock = servicesock;td->_ip = cli_ip;td->_port = cli_port;pthread_t tid;// 多线程不需要关闭文件描述符,因为多线程共享文件描述符pthread_create(&tid, nullptr, threadRoutine, td);}}~TcpServer(){}private:uint16_t _port;std::string _ip;int _listensock; // 监听套接字
};
运行结果:
(2)使用线程池管理线程
引入之前写的线程池,具体代码见Linux知识点 – Linux多线程(四)
线程池代码中:
Task.hpp
更改了回调函数的类型;
#pragma once#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"//typedef std::function<void (int, const std::string&, const uint16_t&, const std::string&)> func_t;//与上面的写法是等价的
using func_t = std::function<void (int, const std::string&, const uint16_t&, const std::string&)>;class Task
{public:Task() {}Task(int sock, const std::string ip, uint16_t port, func_t func) : _sock(sock), _ip(ip), _port(port), _func(func){}void operator()(const std::string &name){_func(_sock, _ip, _port, name);}public:int _sock;std::string _ip;uint16_t _port;func_t _func;
};
tcp_server.hpp
更改了service回调函数和类内成员函数start,每次将任务push进任务队列,等待线程池处理;
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "thread-pool/log.hpp"
#include "thread-pool/threadPool.hpp"
#include "thread-pool/Task.hpp"static void service(int sock, const std::string &cli_ip,const uint16_t &cli_port, const std::string &thread_name)
{// 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)char buffer[1024];while (true){ssize_t s = read(sock, buffer, sizeof(buffer - 1));if (s > 0){buffer[s] = 0;std::cout << thread_name << "|" << cli_ip << ":" << cli_port << "# " << buffer << std::endl;}else if (s == 0) // 对端关闭连接{logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);break;}else{logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));break;}write(sock, buffer, strlen(buffer));}close(sock); // 线程在回调函数中关闭不用的文件描述符
}class TcpServer
{
private:const static int gbacklog = 20;public:TcpServer(uint16_t port, std::string ip = "0.0.0.0"): _listensock(-1), _port(port), _ip(ip), _threadpool_ptr(ThreadPool<Task>::getThreadPool()){}void initServer(){// 1.创建套接字socket -- 进程和文件_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMessage(NORMAL, "create socket success, listensock: %d", _listensock);// 2.bind -- 文件 + 网络struct 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(3);}// 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接if (listen(_listensock, gbacklog) < 0){logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMessage(NORMAL, "init server success");}// 线程池void start(){_threadpool_ptr->run(); // 启动线程池while (true){// 4.获取连接struct sockaddr_in src; // 输出型参数,获取对方主机地址socklen_t len = sizeof src; // 输入输出型参数,对方主机地址的长度int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字if (servicesock < 0){logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));continue; // 获取连接失败后继续获取}// 获取连接成功了,通信对象的地址在accept函数的后两个参数中uint16_t cli_port = ntohs(src.sin_port);std::string cli_ip = inet_ntoa(src.sin_addr);logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",servicesock, cli_ip.c_str(), cli_port);// 线程池版本Task t(servicesock, cli_ip, cli_port, service);_threadpool_ptr->pushTask(t);}}~TcpServer(){}private:uint16_t _port;std::string _ip;int _listensock; // 监听套接字std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 线程池指针
};
其他代码都与之前的一致;
运行结果:
也可以传入其他的回调函数,完成不同的功能;
五、关于地址转换函数
1.字符串转in_addr的函数
inet_aton地址转换函数:
inet_pton地址转换函数
2.in_addr转字符串的函数
- 关于inet_ntoa
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果,因此inet_ntoa是线程不安全的;
在多线程环境下,推荐使用inet_ ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;
六、TCP协议通讯流程
-
服务器初始化
调用socket,创建文件描述符;
调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败;
调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备;
调用accecpt,并阻塞,等待客户端连接过来; -
建立连接的过程(三次握手)
调用socket,创建文件描述符;
调用connect,向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答(第一次);
服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接"(第二次);
客户端收到SYN-ACK后会从connect(返回,同时应答一个ACK段(第三次); -
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同-条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
服务器从accept()返回后立刻调用read),读socket就像读管道一样,如果没有数据到达就阻塞等待;
这时客户端调用write0发送请求给服务器,服务器收到后从read(返回,对客户端的请求进行处理,在此期
间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回,发送下一条请求如此循环下去; -
断开连接的过程(四次挥手)
如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次);
read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次);
客户端收到FIN,再返回一个ACK给服务器(第四次);
相关文章:
Linux知识点 -- 网络编程套接字
Linux知识点 – 网络编程套接字 文章目录 Linux知识点 -- 网络编程套接字一、预备知识1.认识端口号2.套接字3.TCP协议与UDP协议4.网络字节序 二、socket编程接口1.socket常见API2.sockaddr结构 三、UDP套接字编程1.直接打印客户端信息2.执行客户端发来的指令3.多用户聊天4.在wi…...
逆向大漠插件/用VB6.0实现后台鼠标移动和后台鼠标左键点击
自动化设计软件,在一款做门的设计软件CypCut6.3 上实现了自动化勾选了 复选框。一切都是基于后台的。 Private Const GW_CHILD 5 Private Const GW_HWNDFIRST 0 Private Const GW_HWNDNEXT 2 Public Declare Function FindWindow Lib "user32" Alias &…...
重庆OV证书和EV证书有什么区别
SSL数字证书按照保护的域名数量和类型可以分为单域名SSL证书、多域名SSL证书和通配符SSL证书三种,按照验证方式可以将SSL数字证书分为DV基础型SSL证书、OV企业型SSL证书和EV增强型SSL证书三种。今天就随SSL盾小编了解OV证书和EV证书的区别。 1.OV企业型SSL证书由CA…...
uni-app(微信小程序)图片旋转放缩,文字绘制、海报绘制
总结一下: 要进行海报绘制离不开canvas,我们是先进行图片,文字的拖拽、旋转等操作 最后再对canvas进行绘制,完成海报绘制。 背景区域设置为 position: relative,方便图片在当前区域中拖动等处理。添加图片࿰…...
Spring Boot 2.x基础教程
Spring Boot 2.x基础教程 一、简介1. Spring Boot 2.x 简介2. Spring Boot 2.x 特点3. Spring Boot 2.x 与 Spring Framework 的关系 二、Spring Boot 2.x 环境搭建1. JDK环境安装与配置2. Maven环境安装与配置3. Spring Boot 2.x 项目创建 三、核心功能1. 配置文件及其加载顺序…...
汽车红外夜视系统行业发展总体概况
汽车红外夜视系统是一种技术,旨在帮助驾驶员在夜间或低光条件下提供更好的视觉能力。它利用红外光谱的特性来检测和显示在正常光线下难以察觉的热能辐射。这使驾驶员能够在夜间或恶劣天气条件下更好地识别和辨别道路上的物体、行人、动物或其他车辆。 汽车红外夜视…...
Java 和 PHP GC 的差异和差异出现的原因
JAVA 的 GC 处理 判断草死掉的两种方式:引用计数和可达性分析 可达性分析对 JAVA 比较好用的原因是 JAVA遵守这面向对象的严格要求,每个变量都被对象包裹,所以每个变量都能通过对象来进行遍历找到,最终判断他们的是否被引用&…...
loguru logger使用
一、基本使用 ①标准使用 from loguru import logger# 在标准输出里面输出一行debug日志 logger.debug("Thats dubug")②设置输出格式 from loguru import loggerlogger.remove(0) # 先删除格式 logger.add(sink./logger.log, format"{time: %Y-%m-%d %H:%M…...
vue-自适应布局-postcss-pxtorem
原理: 比如一个375px设计稿 其中一个320px宽度的元素 如何实现自适应布局呢? 其实可以这样理解: 我们先计算出375屏幕时候320px的大小,在屏幕变化时候,这些元素都会等比例缩放 比如屏幕从375 变为750px时候࿰…...
9.12|day 5|day 44 |完全背包| 518. 零钱兑换 II | 377. 组合总和 Ⅳ
● 完全背包 主要是看清01背包和完全背包的区别 //01背包 for(int i 0;i<weight.size();i){ for(int j bagWeight;j>weight[i];j--){dp[j] Math.max(dp[j],dp[j-weight[i]]value[i]); } } //完全背包 for(int i 0;i<weight.size();i){for(int j weight[i];j<…...
C++ 中的原子变量(std::atomic)使用指南
目录 C 中的原子变量(std::atomic)使用指南基本概念使用方法创建原子变量读取值修改值原子操作 常见应用场景1. 计数器2. 控制标志3. 链表和数据结构 示例代码结论 C 中的原子变量(std::atomic)使用指南 原子变量(std…...
【用unity实现100个游戏之9】使用Unity制作类八方旅人、饥荒风格的俯视角2.5D游戏
前言 2.5D游戏 是一种介于二维和三维之间的游戏形式。它通常在二维平面上展示游戏内容,但利用三维技术来实现更加逼真的图像效果。 在2.5D游戏中,角色和环境通常是以平面的形式呈现,但可以在垂直方向上移动。这意味着玩家可以在一个相对较薄…...
如何在群晖中,正确配置 docker 的 ipv6 地址
参考 2023年9月12日 https://synocommunity.com/ https://github.com/wangliangliang2/fix_synology_docker_ipv6 https://post.smzdm.com/p/an3np8m7/ 正文 关于这个话题,国内搜索引擎得到的结果出奇的一致,且过时。 (看的我脑壳痛&#…...
XSS入门 XSS Challenges
level1(直接注入) <script>alert(xss)</script>level2(双引号闭合标签) 测试 <sCr<ScRiPt>IPT>OonN"\/(hrHRefEF)</sCr</ScRiPt>IPT>发现<>"被转换,构造新的语句 "><script>alert(/xss/)</…...
李沐《动手学深度学习》torch.cat() 和 torch.stack()的区别及思考
一、问题引出 好久没更新啦!最近在学习沐神《动手学深度学习》6.5节池化层的时候,发现沐神在两处相似的地方使用了两种Python拼接函数torch.cat()和torch.stack(): 百思不得其解,于是查阅相关文档之后终于弄清楚了两者之间的区别…...
【算法与数据结构】235、LeetCode二叉搜索树的最近公共祖先
文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析:本题和这道题类似【算法与数据结构】236、LeetCode二叉树的最近公共祖先,相同的算法也能解…...
bboss 流批一体化框架 与 数据采集 ETL
数据采集 ETL 与 流批一体化框架 特性: 高效、稳定、快速、安全 bboss 是一个基于开源协议 Apache License 发布的开源项目,主要由以下三部分构成: Elasticsearch Highlevel Java Restclient , 一个高性能高兼容性的Elasticsea…...
JVM详细教程
JVM 前言 还在完善中先发布 JVM虚拟机厂家多钟多样,具体实现细节可能不一样,这里主要讲的是虚拟机的规范,以下内容融合了各个平台发布的内容和周志明老师的《深入理解java虚拟机》 JVM概述 如何理解jvm跨平台? 编译成汇编代码…...
Smartbi吴华夫:后疫情时代,BI发展趋势的观察与应对
沿着旧地图找不到新大陆,“基于指标体系的可视化分析和增强分析”成为BI发展新阶段。Smartbi V11系列新品与时俱进,以指标为核心,同时融合BI应用,赋能管理者和业务,成为引领数字化运营的新航标! ——思迈特…...
软件设计模式系列之三———工厂方法模式
1 模式的定义 工厂方法模式是一种常见的设计模式,属于创建型设计模式之一,它在软件工程中用于对象的创建。该模式的主要思想是将对象的创建过程抽象化,将具体对象的实例化延迟到子类中完成,以便在不同情况下可以创建不同类型的对…...
pytorch 多卡分布式训练 调用all_gather_object 出现阻塞等待死锁的问题
pytorch 多卡分布式训练 torch._C._distributed_c10d中的函数all_gather_object 出现阻塞等待死锁的问题 解决办法就是 在进程通信之前调用torch.cuda.set_device(local_rank) For NCCL-based processed groups, internal tensor representations of objects must be moved …...
SpringMvc增删改查
SpringMvc增删改查 一、前期准备二、逆向生成增删改查2.2.aspect切面层2.3.Mybatis generator逆向生成2.4.根据生成代码编写Biz层与实现类 三、controller层代码编写四、前台代码与分页代码五、案例测试 一、前期准备 1.2.导入pom.xml依赖 <?xml version"1.0" …...
【计算机网络】网络编程接口 Socket API 解读(5)
Socket 是网络协议栈暴露给编程人员的 API,相比复杂的计算机网络协议,API 对关键操作和配置数据进行了抽象,简化了程序编程。 本文讲述的 socket 内容源自 Linux man。本文主要对各 API 进行详细介绍,从而更好的理解 socket 编程。…...
手动实现一个bind函数!
原文地址:手动实现一个bind函数! - 知乎 1.bind函数用法 bind()方法用于创建一个新的函数,这个新函数接收的第一个参数代表的就是this,利用bind()函数我就就可以任意改变函数内部的this指向了。 官网的解释: bind()…...
数据结构-时间复杂度/空间复杂度
Hello,好久没有更新了哦,已经开始学习数据结构了,这篇文章呢就是对刚学数据结构所接触到的时间复杂度进行一个分享哦,如果有错误之处,大家记得拍拍我哦~ 既然要讨论时间/空间复杂度,那我们就得知道时间/空…...
英语写作中“展示”、“表明”demonstrate、show、indicate、illustrate的用法
一、demonstrate、show、indicate在论文写作中主要用法是:demonstrate/show/indicate 从句: Sb./Sth. demonstrates/shows/indicates that ……从句中一般表达事实、观点和结论等。 例句: The authors demonstrated/showed/indicated that…...
Redis的java客户端
在Redis官网中提供了各种语言的客户端,地址:https://redis.io/resources/clients/ redis的java客户端 https://redis.io/resources/clients/#java 1.jedis使用 引入依赖 <dependency><groupId>redis.clients</groupId><artifac…...
Android环境配置笔记
文章目录 一、各环境文档二、参考 一、各环境文档 Gradle官方的兼容性文档:Java Compatibility 更新日期:2023.9.12 Android Gradle插件版本:Android Gradle Plugin 二、参考 参考文章:Android问题记录...
element-table 行的拖拽更改顺序(无需下载sortableJs
样例展示:vueelement 通过阅读element文档我们发现element并不提供拖拽相关的api 本博客通过element提供的行类名 注册函数 实现行与行的拖拽 1.设置el-table 的行样式类名 这里是用的是 function <el-table:data"outputData":row-class-name&qu…...
Docker部署jenkins
目录 一、jenkins原理二、Docker部署jenkins1.下载jenkins镜像文件2.查看下载的jenkins镜像3.创建Jenkins挂载目录并授权权限4.创建并启动Jenkins容器5.查看jenkins是否启动成功6.查看docker容器日志7.配置镜像加速8.访问Jenkins页面,输入ip地址加上9000端口9.获取管…...
深圳公司网站推广/百度基木鱼建站
universal reference If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference. 如果一个变量或参数声明为T&&类型,且T为推导类型,则这个变量或者参数…...
无锡网站建设营销型/找培训班一般在什么平台
http://tech.it168.com/a2011/1122/1277/000001277070.shtml 对于Android应用开发来说,手机铃声是一个非常重要的需求,网上查了很多例子,都有点问题,综合几个自己写了个可以设置铃声、通知声音、闹钟声音和所有声音功能的方法。 …...
链家在线网站是哪个公司做的/百度一下你就知道了官网
首先注册一个代理(firewall): options->global options->firewall->add->socks5 在会话中设置成当前的代理: session->Session Options ->ssh2,在下拉菜单选择代理。...
网站开发合同样本/免费涨1000粉丝网站
最近在想自己的文章有些是不是写的太难以理解了呢.........竟然好多人看了还是会直接问我很多问题....... 其实PID哈靠自己想像就能自己写出来自己的代码,也许是网上的讲的太过的高深什么积分微分,搞的晕头转向,本来这么实用的想法为什么偏偏说的那么的琢磨不透......感觉那些人…...
做微信h5的网站/免费网站安全检测
概述 什么是可靠消息最终一致性事务 可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一…...
自己做的网站怎么爬数据库/crm软件
引言: Dos.Common是一个开发中的常用类库,如HttpHelper、LogHelper、CacheHelper、CookieHelper、MapperHelper等等。与Dos.WeChat、Dos.CMS等集成。官方网站:http://ITdos.com/Dos/Common/Index.html 开源中国:http://www.oschin…...