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

CS 144 Lab Seven -- putting it all together

CS 144 Lab Seven -- putting it all together

  • 引言
  • 测试
  • lab7.cc
    • UDPSocket
    • NetworkInterfaceAdapter
    • TCPSocketLab7
    • main方法
    • 子线程
  • 小结


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Six 对应的PDF: Checkpoint 6: putting it all together


引言

本实验无需进行任何编码操作,同时我们还可以在这个实验中,将之前6个实验里所有实现的内容全部粘合在一起,并与真实网络进行通信。

在这里插入图片描述


测试

在两个终端分别执行以下两个命令:

./apps/lab7 server cs144.keithw.org 3000
./apps/lab7 client cs144.keithw.org 3001

便可以看到两个服务成功相互连接:

在这里插入图片描述
在这里插入图片描述


lab7.cc

lab seven的测试文件为lab7.cc,借助该测试文件,我们来看一下如何将lab six实现的Router也加入进来共同运作的。

首先我们先来看一下相关工具类和工具方法的实现:

  • random_host_ethernet_address: 为主机生成随机的MAC地址
EthernetAddress random_host_ethernet_address() {EthernetAddress addr;for (auto &byte : addr) {byte = rd();  // use a random local Ethernet address}addr.at(0) |= 0x02;  // "10" in last two binary digits marks a private Ethernet addressaddr.at(0) &= 0xfe;return addr;
}
  • random_router_ethernet_address: 为路由器生成随机的MAC地址
EthernetAddress random_router_ethernet_address() {EthernetAddress addr;for (auto &byte : addr) {byte = rd();  // use a random local Ethernet address}addr.at(0) = 0x02;  // "10" in last two binary digits marks a private Ethernet addressaddr.at(1) = 0;addr.at(2) = 0;return addr;
}

MAC地址确实有私有和全球唯一(公有)之分:

  1. 全球唯一MAC地址(全局唯一地址): 这是由IEEE(Institute of Electrical and Electronics Engineers)组织分配的唯一MAC地址,通常称为全球唯一MAC地址或全局唯一地址。全球唯一MAC地址由厂商分配给网络设备,确保在全球范围内没有两个设备使用相同的MAC地址。前三个字节表示厂商识别码(OUI),后三个字节由厂商自行分配。全球唯一MAC地址用于在互联网和广域网等大范围的网络中唯一标识设备。

  2. 本地MAC地址(私有地址): 本地MAC地址也称为私有MAC地址,是用于在局域网(LAN)内部使用的MAC地址。本地MAC地址的第一个字节通常是02060A0E,这些开头的地址被称为本地管理员地址(Locally Administered Addresses)。本地MAC地址通常不需要在全球范围内唯一,因为它们只在局域网内部使用。局域网内的设备可以自行分配本地MAC地址,只要确保在局域网内不会产生冲突即可。

全球唯一MAC地址和本地MAC地址之间的区别在于其范围和分配方式。全球唯一MAC地址由IEEE控制分配,确保在全球范围内唯一,用于在大范围的网络中进行全球性标识。而本地MAC地址是在局域网内部使用的,可以由设备自行分配,只需要在局域网内部保持唯一即可。


UDPSocket

在这里插入图片描述

  • LocalStreamSocket: 这个类在Lab four和Lab five中都间接涉及到了,该Socket子类用于本地两个进程间的通信处理,借助socketpair这个系统调用创建的一对相互连接的套接字完成
  • UDPSocket: 对本机Linux网络子系统提供的UDP socket进行的包装
  • TCPSocket: 对本机Linux网络子系统提供的TCP socket进行的包装

这里我们来看一下UDPSocket的实现:

socket.hh:

//! A wrapper around [UDP sockets](\ref man7::udp)
// 对本机Linux网络子系统提供的UDP socket进行的包装
class UDPSocket : public Socket {protected://  Construct from FileDescriptor (used by TCPOverUDPSocketAdapter)//  fd is the FileDescriptor from which to constructexplicit UDPSocket(FileDescriptor &&fd) : Socket(std::move(fd), AF_INET, SOCK_DGRAM) {}public://! Default: construct an unbound, unconnected UDP socket// 无参构造,默认创建出来的是UDP socketUDPSocket() : Socket(AF_INET, SOCK_DGRAM) {}//! Returned by UDPSocket::recv; carries received data and information about the sender// recv接收UDP数据报方法返回结果struct received_datagram {Address source_address;  // Address from which this datagram was receivedstd::string payload;     // UDP datagram payload};//! Receive a datagram and the Address of its senderreceived_datagram recv(const size_t mtu = 65536);//! Receive a datagram and the Address of its sender (caller can allocate storage)void recv(received_datagram &datagram, const size_t mtu = 65536);//! Send a datagram to specified Addressvoid sendto(const Address &destination, const BufferViewList &payload);//! Send datagram to the socket's connected address (must call connect() first)void send(const BufferViewList &payload);
};
  • 如果UDPSocket使用无参构造初始化,那么最终会调用父类Socket对象的构造函数初始化一个使用IPV4协议和UDP协议的Socket:
// default constructor for socket of (subclassed) domain and type
//! \param[in] domain is as described in [socket(7)](\ref man7::socket), probably `AF_INET` or `AF_UNIX`
//! \param[in] type is as described in [socket(7)](\ref man7::socket)
Socket::Socket(const int domain, const int type) : FileDescriptor(SystemCall("socket", socket(domain, type, 0))) {}

socket 系统调用用于创建一个新的套接字,下面是对每个参数的解释:

  • domain: 套接字的协议域(也称为地址族),指定了套接字的通信范围和协议类型。例如,AF_INET 表示 IPv4 地址族,AF_UNIX 表示本地套接字(Unix 域套接字)。这个参数决定了套接字将在哪种网络层协议上工作。
  • type: 套接字的类型,指定了套接字的通信方式。例如,SOCK_STREAM 表示流式套接字(用于 TCP),SOCK_DGRAM 表示数据报套接字(用于 UDP)。这个参数决定了套接字将如何进行数据传输。
  • 0: 这是套接字的选项标志,通常设置为 0,表示不使用任何特定的选项。

如果socket构造函数指明了fd , 并且fd实际指向一个tun设备 ,那么在构造函数中,代码会验证 TUN 设备的协议域和类型是否与预期的值一致。这是因为 TUN 设备在内核中被实现为一个虚拟网络设备,有关于其属性的信息可以通过套接字选项来获取。

在这个情境下,getsockopt 调用用于获取 TUN 设备的协议域和类型。如果 TUN 设备的实际协议域或类型与期望的不匹配,那么将抛出异常,表示套接字不满足所需的属性。

Socket::Socket(FileDescriptor &&fd, const int domain, const int type) : FileDescriptor(move(fd)) {int actual_value;socklen_t len;// verify domainlen = sizeof(actual_value);SystemCall("getsockopt", getsockopt(fd_num(), SOL_SOCKET, SO_DOMAIN, &actual_value, &len));if ((len != sizeof(actual_value)) or (actual_value != domain)) {throw runtime_error("socket domain mismatch");}// verify typelen = sizeof(actual_value);SystemCall("getsockopt", getsockopt(fd_num(), SOL_SOCKET, SO_TYPE, &actual_value, &len));if ((len != sizeof(actual_value)) or (actual_value != type)) {throw runtime_error("socket type mismatch");}
}

socket.cc:

  • recv: 调用udp socket的recvfrom接收外网传入的udp数据报(此处说的是Linux网络子系统中提供的udp socket)
//! \note If `mtu` is too small to hold the received datagram, this method throws a std::runtime_error
// 将接收到的UDP数据报存储到datagram中
void UDPSocket::recv(received_datagram &datagram, const size_t mtu) {// receive source address and payload// 用于接收数据报来源地址Address::Raw datagram_source_address;datagram.payload.resize(mtu);socklen_t fromlen = sizeof(datagram_source_address);// 通过系统调用,调用本机Linux网络子系统中socket提供的recvfrom接口const ssize_t recv_len = SystemCall("recvfrom",::recvfrom(// 哪个socket,接收的数据存储到哪里,接收缓冲区的大小,接收标志,表示如果数据报过大会截断,并返回截断后的数据。如果不指定这个标志,过大的数据报会被丢弃// 用于存储源地址的缓冲区,源地址缓冲区的大小 fd_num(), datagram.payload.data(), datagram.payload.size(), MSG_TRUNC, datagram_source_address, &fromlen));// 如果接收到的数据大小超过了mtu,则抛出异常if (recv_len > ssize_t(mtu)) {throw runtime_error("recvfrom (oversized datagram)");}register_read();// 记录数据包来源地址datagram.source_address = {datagram_source_address, fromlen};// 调整payload缓冲区大小为实际接收到的数据量datagram.payload.resize(recv_len);
}UDPSocket::received_datagram UDPSocket::recv(const size_t mtu) {received_datagram ret{{nullptr, 0}, ""};recv(ret, mtu);return ret;
}
  • sendmsg_helper: 调用udp socket的sednmsg将准备好的UDP数据包发送出去(此处说的是Linux网络子系统中提供的udp socket)
// 发送UDP数据报: socket描述符,存放目的地址的缓冲区,缓冲区大小,要发送的数据载荷
void sendmsg_helper(const int fd_num,const sockaddr *destination_address,const socklen_t destination_address_len,const BufferViewList &payload) {auto iovecs = payload.as_iovecs();// 构建数据包msghdr message{};message.msg_name = const_cast<sockaddr *>(destination_address);message.msg_namelen = destination_address_len;message.msg_iov = iovecs.data();message.msg_iovlen = iovecs.size();// 通过系统调用sendmsg完成数据包的发送const ssize_t bytes_sent = SystemCall("sendmsg", ::sendmsg(fd_num, &message, 0));// 检验成功发送的字节数和payload大小是否一致,也就是数据包是否成功发送 if (size_t(bytes_sent) != payload.size()) {throw runtime_error("datagram payload too big for sendmsg()");}
}// 发送时指明目的地址
void UDPSocket::sendto(const Address &destination, const BufferViewList &payload) {sendmsg_helper(fd_num(), destination, destination.size(), payload);register_write();
}// 发送时不指定目的地址
void UDPSocket::send(const BufferViewList &payload) {sendmsg_helper(fd_num(), nullptr, 0, payload);register_write();
}

NetworkInterfaceAdapter

  • 为了适配从通道读写IP数据报
  • lab five中通道被写死为了Tap设备,但是为了解耦,这里采用了双向通道,这样方便切换底层网络驱动实现
// 为了适配从通道读写IP数据报
class NetworkInterfaceAdapter : public TCPOverIPv4Adapter {private:// 网络接口NetworkInterface _interface;// 下一条IP地址Address _next_hop;// socket_pair系统调用创建出来的本地套接字双向通信通道 --> lab five的测试文件中,这里是写死为Tap设备// 但是此处我们利用双向通道进行解耦,这样数据可以来源于Tap设备,也可以来源于其他地方 -- 解耦pair<FileDescriptor, FileDescriptor> _data_socket_pair = socket_pair_helper(SOCK_DGRAM);// 将网络接口输出队列中等待输出的以太网帧取出,然后写入双向通信通道中void send_pending() {while (not _interface.frames_out().empty()) {_data_socket_pair.first.write(_interface.frames_out().front().serialize());_interface.frames_out().pop();}}public:NetworkInterfaceAdapter(const Address &ip_address, const Address &next_hop):// 当前网络接口MAC地址采用随机生成,ip地址采用传入的 _interface(random_host_ethernet_address(), ip_address), // 下一条IP地址也是采用传入的_next_hop(next_hop) {}// 从通道读取数据optional<TCPSegment> read() {EthernetFrame frame;// 解析从通道读取得到的以太网帧if (frame.parse(_data_socket_pair.first.read()) != ParseResult::NoError) {return {};}// Give the frame to the NetworkInterface. Get back an Internet datagram if frame was carrying one.// 交给网络接口处理,得到IP数据报optional<InternetDatagram> ip_dgram = _interface.recv_frame(frame);// The incoming frame may have caused the NetworkInterface to send a frame// 将网络接口中待发送的以太网帧一股脑发送到通道中send_pending();// Try to interpret IPv4 datagram as TCP// 如果是IP数据报,那么剥离得到TCP segment,然后返回if (ip_dgram) {return unwrap_tcp_in_ip(ip_dgram.value());}return {};}// 向网络接口写入TCP数据段,网络接口将处理完毕的数据段写入_segment_out输出队列// 然后调用send_pending将队列中带输出的以太网帧写入通道void write(TCPSegment &seg) {// tcp段加上IP头_interface.send_datagram(wrap_tcp_in_ip(seg), _next_hop);send_pending();}// _tcp_loop会不间断调用当前适配器的tickvoid tick(const size_t ms_since_last_tick) {_interface.tick(ms_since_last_tick);send_pending();}NetworkInterface &interface() { return _interface; }queue<EthernetFrame> frames_out() { return _interface.frames_out(); }// 运算符重载,用于事件循环判获取first,从而判断当前NetworkInterfaceAdapter是否可读可写operator FileDescriptor &() { return _data_socket_pair.first; }FileDescriptor &frame_fd() { return _data_socket_pair.second; }
};

在这里插入图片描述


TCPSocketLab7

  • 对NetworkInterfaceAdapter的适配,同时在父类TCPSpongeSocket基础上增加一些参数合法检测和方法,使其更符合标准Socket接口
class TCPSocketLab7 : public TCPSpongeSocket<NetworkInterfaceAdapter> {Address _local_address;public:TCPSocketLab7(const Address &ip_address, const Address &next_hop): TCPSpongeSocket<NetworkInterfaceAdapter>(NetworkInterfaceAdapter(ip_address, next_hop)), _local_address(ip_address) {}// client建立连接--参数: 连接的server的地址void connect(const Address &address) {FdAdapterConfig multiplexer_config;// 客户端的启动端口随机采用_local_address = Address{_local_address.ip(), uint16_t(random_device()())};cerr << "DEBUG: Connecting from " << _local_address.to_string() << "...\n";// multiplexer_config保存源地址和目的地址multiplexer_config.source = _local_address;multiplexer_config.destination = address;// 调用父类的Connect方法TCPSpongeSocket<NetworkInterfaceAdapter>::connect({}, multiplexer_config);}// server绑定端口void bind(const Address &address) {// 我们只能指定port,ip是固定的if (address.ip() != _local_address.ip()) {throw runtime_error("Cannot bind to " + address.to_string());}_local_address = Address{_local_address.ip(), address.port()};}// server监听端口void listen_and_accept() {FdAdapterConfig multiplexer_config;multiplexer_config.source = _local_address;// 调用父类listen_and_accept方法TCPSpongeSocket<NetworkInterfaceAdapter>::listen_and_accept({}, multiplexer_config);}NetworkInterfaceAdapter &adapter() { return _datagram_adapter; }
};

main方法

int main(int argc, char *argv[]) {try {if (argc <= 0) {abort();  // For sticklers: don't try to access argv[0] if argc <= 0.}if (argc != 4 and argc != 5) {print_usage(argv[0]);return EXIT_FAILURE;}if (argv[1] != "client"s and argv[1] != "server"s) {print_usage(argv[0]);return EXIT_FAILURE;}// 启动程序主体 program_body(argv[1] == "client"s, argv[2], argv[3], argc == 5);} catch (const exception &e) {cerr << e.what() << "\n";return EXIT_FAILURE;}return EXIT_SUCCESS;
}

lab7测试程序的主体:

// lab7测试程序的主体
// 参数: 当前启动的是客户端和服务端,
void program_body(bool is_client, const string &bounce_host, const string &bounce_port, const bool debug) {// 连接外网的udp socket(通过本地linux网络子系统构建得到的udp socket)UDPSocket internet_socket;// 外部帮忙中转数据包的serverAddress bounce_address{bounce_host, bounce_port};/* let bouncer know where we are */// 让bouncer知道我们是谁internet_socket.sendto(bounce_address, "");internet_socket.sendto(bounce_address, "");internet_socket.sendto(bounce_address, "");/* set up the router */// client和server各自都有一个默认路由器 -- lab six我们实现的路由器Router router;// host_side代表的网络接口为连接主机所在内网的一端// internet_side代表的网络接口为连接外网的一端unsigned int host_side, internet_side;// 启动的是客户端if (is_client) {// 向路由器中添加两个网络接口// 1.该网络接口处于client主机所在子网host_side = router.add_interface({random_router_ethernet_address(), {"192.168.0.1"}});// 2.该网络接口连接广域网internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.192"}});// 向路由器添加路由条目: 路由前缀,前缀长度,下一条IP地址,网络接口索引// 如果路由前缀所在子网与当前传入的网络接口处在同一个网络,则目的IP地址为空router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, {}, host_side);router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, Address{"10.0.0.172"}, internet_side);} else {// 启动的是服务端    host_side = router.add_interface({random_router_ethernet_address(), {"172.16.0.1"}});internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.172"}});router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, {}, host_side);router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, Address{"10.0.0.192"}, internet_side);}/* set up the client */TCPSocketLab7 sock =is_client ? // 客户端主机IP地址和下一条的IP地址(默认路由)TCPSocketLab7{{"192.168.0.50"}, {"192.168.0.1"}} : // 服务端主机IP地址和下一条的IP地址(默认路由)TCPSocketLab7{{"172.16.0.100"}, {"172.16.0.1"}};atomic<bool> exit_flag{};/* set up the network */// 启动一个子线程thread network_thread([&]() {// 子线程需要干的事情...});try {// 如果当前启动的是客户端,则调用sock的connectif (is_client) {sock.connect({"172.16.0.100", 1234});} else {// 如果启动的是服务端,向绑定ip和端口,然后开启监听    sock.bind({"172.16.0.100", 1234});sock.listen_and_accept();}// 开启标准输入,标准输出与socket之间的双向复制bidirectional_stream_copy(sock);sock.wait_until_closed();} catch (const exception &e) {cerr << "Exception: " << e.what() << "\n";}cerr << "Exiting... ";exit_flag = true;network_thread.join();cerr << "done.\n";
}

子线程

上面将program_body函数中子线程需要干的事情注释掉了,因为子线程干的事情是重点,所以这里单独拎出来看:

// 启动一个子线程thread network_thread([&]() {try {// 初始化事件循环EventLoop event_loop;// Frames from host to router// _data_socket_pair通道可读事件event_loop.add_rule(sock.adapter().frame_fd(), Direction::In, [&] {EthernetFrame frame;// 从通道读取以太网帧if (frame.parse(sock.adapter().frame_fd().read()) != ParseResult::NoError) {return;}if (debug) {cerr << "     Host->router:     " << summary(frame) << "\n";}// 交给对应路由器进行路由// 1.先找到当前主机端对应的网络接口,让其接收以太网帧,处理后暂存队列router.interface(host_side).recv_frame(frame);// 2.进行路由router.route();});// Frames from router to host// _data_socket_pair通道可写事件event_loop.add_rule(sock.adapter().frame_fd(),Direction::Out,[&] {// 从路由器取出当前主机端的网络接口,获取其待输出队列// 该输出队列暂存待发送以太网帧// 由于lab seven构造的每个主机所在的局域网只有他自己,所以这里的以太网帧就是发送给当前主机的auto &f = router.interface(host_side).frames_out();if (debug) {cerr << "     Router->host:     " << summary(f.front()) << "\n";}// 将数据包写入通道,即发送以太网帧给当前主机自己sock.adapter().frame_fd().write(f.front().serialize());f.pop();},[&] { return not router.interface(host_side).frames_out().empty(); });// Frames from router to Internet// internet_socket可写事件event_loop.add_rule(internet_socket,Direction::Out,[&] {// 从当前路由器取出连接广域网的网络接口,然后获取它的待输出数据包队列auto &f = router.interface(internet_side).frames_out();if (debug) {cerr << "     Router->Internet: " << summary(f.front()) << "\n";}// 将数据包发送到外部server服务器internet_socket.sendto(bounce_address, f.front().serialize());f.pop();},[&] { return not router.interface(internet_side).frames_out().empty(); });// Frames from Internet to router// internet_socket可读事件event_loop.add_rule(internet_socket, Direction::In, [&] {EthernetFrame frame;// 从internet_socket读取出以太网数据包if (frame.parse(internet_socket.read()) != ParseResult::NoError) {return;}if (debug) {cerr << "     Internet->router: " << summary(frame) << "\n";}// 然后将数据包交给连接广域网的网络接口进行接收,处理完后暂存队列router.interface(internet_side).recv_frame(frame);// 将队列中待路由的数据包取出进行路由发送router.route();});// 开启事件循环,并且一直轮询while (true) {// 每次最多等待50毫秒if (EventLoop::Result::Exit == event_loop.wait_next_event(50)) {cerr << "Exiting...\n";return;}// 定时调用tick方法router.interface(host_side).tick(50);router.interface(internet_side).tick(50);if (exit_flag) {return;}}} catch (const exception &e) {cerr << "Thread ending from exception: " << e.what() << "\n";}});

此处的事件循环相较于lab four而言更加复杂,数据读写过程共涉及三个事件循环公共协作完成,首先我们来看一下键盘输入数据发送的整个流程:
在这里插入图片描述

从网络接收到udp数据包,并从udp数据包的payload中获取以太网帧,然后将以太网帧传送给Router,经过一系列步骤后,最终回显到屏幕的整个过程如下:
在这里插入图片描述
有一点需要注意,我们最终是借助本机linux操作系统提供的udp socket完成数据包的发送,并且该udp数据包的payload载荷是我们封装好的以太网帧:
在这里插入图片描述

此时,我们在来回看一开始给出的这张协作图,或许就没有那么难以理解了:
在这里插入图片描述


小结

本节作为cs144课程lab终章,给出了笔者个人对于整个组合过程的理解,肯定存在理解偏差之处,欢迎各位大佬在评论区指出错误或给予补充。

CS144作为计算机网络的入门课程,下面是一些CS计网相关的进阶课程:

  • CS155(计算机与网络安全)
  • CS244(网络高级主题)
  • CS249i(现代互联网)

相关文章:

CS 144 Lab Seven -- putting it all together

CS 144 Lab Seven -- putting it all together 引言测试lab7.ccUDPSocketNetworkInterfaceAdapterTCPSocketLab7main方法子线程 小结 对应课程视频: 【计算机网络】 斯坦福大学CS144课程 Lab Six 对应的PDF: Checkpoint 6: putting it all together 引言 本实验无需进行任何编…...

opencv基础-29 Otsu 处理(图像分割)

Otsu 处理 Otsu 处理是一种用于图像分割的方法&#xff0c;旨在自动找到一个阈值&#xff0c;将图像分成两个类别&#xff1a;前景和背景。这种方法最初由日本学者大津展之&#xff08;Nobuyuki Otsu&#xff09;在 1979 年提出 在 Otsu 处理中&#xff0c;我们通过最小化类别内…...

gcc-buildroot-9.3.0 和 gcc-arm-10.3 的区别

gcc-buildroot-9.3.0 和 gcc-arm-10.3 是两个不同的 GCC (GNU Compiler Collection) 版本&#xff0c;主要用于编译 C、C 和其他语言的程序。它们之间的区别主要体现在以下几个方面&#xff1a; 版本号&#xff1a;gcc-buildroot-9.3.0 对应的是 GCC 9.3.0 版本&#xff0c;而 …...

IDEA Run SpringBoot程序步骤原理

这个文章不是高深的原理文章&#xff0c;仅仅是接手一个外部提供的阉割版代码遇到过的一个坑&#xff0c;后来解决了&#xff0c;记录一下。 1、IDEA Run 一个SpringBoot一直失败&#xff0c;提示找不到类&#xff0c;但是maven install成功&#xff0c;并且java -jar能成功ru…...

海康威视摄像头配置RTSP协议访问、onvif协议接入、二次开发SDK接入

一、准备工作 (1)拿到摄像头之后,将摄像头电源线插好,再将网线插入到路由器上。 (2)将自己的笔记本电脑也连接到路由器网络,与摄像头出在同一个局域网。 二、配置摄像头 2.1 激活方式选择 第一次使用设备需要激活,在进行配置。 最简单,最方便的方式是选择浏览器激…...

Android中的Parcelable 接口

Android中的Parcelable 接口 在Android中&#xff0c;Parcelable接口是用于实现对象序列化和反序列化的一种机制。它允许我们将自定义的Java对象转换成一个可传输的二进制数据流&#xff0c;以便在不同组件之间传递数据。通常在Activity之间传递复杂的自定义对象时&#xff0c…...

Docker-Compose编排与部署

目录 Docker Compose Compose的优点 编排和部署 Compose原理 Compose应用案例 安装docker-ce 阿里云镜像加速器 安装docker-compose docker-compose用法 Yaml简介 验证LNMP环境 Docker Compose Docker Compose 的前身是 Fig&#xff0c;它是一个定义及运行多个 Dock…...

Linux JDK 安装

文章目录 安装步骤1、卸载openJDK1.1 查看当前Linux系统是否安装java,卸载openjdk1.2 卸载系统中已经存在的openJDK 2、在/usr/local目录下创建java目录3、上传JDK到Linux系统4、解压jdk5、配置Jdk环境变量6、重新加载/etc/profile文件&#xff0c;让配置生效7、测试安装是否成…...

JS中常用的数组拷贝技巧

我们都知道&#xff0c;数组也是属于对象&#xff0c;在JS中对象的存储方式则是引用的方式。我们想要拷贝一个数组&#xff0c;就不能只是变量之前的赋值拷贝&#xff0c;这样他们将共享同一个引用&#xff0c;而数组又具有可变性&#xff0c;所以无法将原数组和拷贝的数组的数…...

SAP ABAP程序性能优化-养成良好的代码习惯

ABAP程序基本上都需要从数据库里面抓数&#xff0c;所以性能很重要&#xff0c;同时有一些基本的&#xff0c;和优秀的写法是我们必须要掌握的&#xff0c;不然就会造成程序性能很差。下面给予总结&#xff08;这里包括有很基本的&#xff0c;也包括有比较少用到的&#xff09;…...

SQL SERVER ip地址改别名

SQL server在使用链接服务器时必须使用别名&#xff0c;使用ip地址就会把192.188.0.2这种点也解析出来 解决方案&#xff1a; 1、物理机ip 192.168.0.66 虚拟机ip 192.168.0.115 2、在虚拟机上找到 C:\Windows\System32\drivers\etc 下的 &#xff08;我选中的文件&a…...

数据结构-1

1.2 线性结构树状结构网状结构&#xff08;表 数 图&#xff09; 数据&#xff1a;数值型 非数值型 1.2.3数据类型和抽象数据类型 1.3抽象数据类型 概念小结&#xff1a; 线性表: 如果在独立函数实现的 .c 文件中需要包含 stdlib.h 头文件&#xff0c;而主函数也需要包含 st…...

Java自定义校验注解实现List、set集合字段唯一性校验

文章目录 一&#xff1a; 使用场景二&#xff1a; 定义FieldUniqueValid注解2.1 FieldUniqueValid2.2 注解说明2.3 Constraint 注解介绍2.4 FieldUniqueValid注解使用 三&#xff1a;自定义FieldUniqueValidator校验类3.1 实现ConstraintValidator3.2 重写initialize方法3.3 重…...

xiaoweirobot.chat

目录 1 xiaoweirobot.chat 1.1 DetailList 2 HttpData 2.1 doInBackground 2.2 onPostExecute xiaoweirobot.chatpackage com.shrimp.xiaoweirobot.chat; DetailList <...

【无公网IP】本地电脑搭建个人博客网站(并发布公网访问 )和web服务器

【无公网IP】本地电脑搭建个人博客网站&#xff08;并发布公网访问 &#xff09;和web服务器 文章目录 【无公网IP】本地电脑搭建个人博客网站&#xff08;并发布公网访问 &#xff09;和web服务器前言1. 安装套件软件2. 创建网页运行环境 指定网页输出的端口号3. 让WordPress在…...

SpringCloud(29):Nacos简介

1 什么是配置中心 1.1 什么是配置 应用程序在启动和运行的时候往往需要读取一些配置信息&#xff0c;配置基本上伴随着应用程序的整个生命周期&#xff0c;比如&#xff1a;数据库连接参数、启动参数等。 配置主要有以下几个特点&#xff1a; 配置是独立于程序的只读变量 …...

freeBSD - 笔记

1 介绍 FreeBSD&#xff1a; FreeBSD是由FreeBSD项目团队开发的&#xff0c;最早可以追溯到1993年。它专注于性能、稳定性和可靠性&#xff0c;并在服务器和高性能计算环境中广泛使用。FreeBSD有着强大的网络性能和高度优化的TCP/IP协议栈&#xff0c;因此在网络服务器领域表…...

【Linux】网络基础——宏观认识计算机网络

1 计算机网络背景 网络发展 独立模式: 计算机之间相互独立; 一开始&#xff0c;计算机发明出来之后&#xff0c;一台计算机处理完的数据&#xff0c;数据会保存在软盘&#xff08;物理&#xff09;&#xff0c;通过人之间的相互通信&#xff0c;把计算机A处理完的数据存储到软…...

数字人现身大运会,怎么以动作捕捉技术助推运动与文博相结合

中国移动动感地带数字人橙络络&#xff0c;作为数智体验官以元宇宙的视角&#xff0c;带领观众沉浸式体验大运会&#xff0c;以极具科技和未来的数字人&#xff0c;对外传递大运青春风采&#xff0c;并且数字人橙络络还对大运会的赛事、活动进行了科普、讲解以及表演当地特色才…...

WSL安装

WSL安装 1.Microsoft store 安装 1.1 启动WSL功能 在【程序和功能 -> 启用或关闭 Windows 功能】中勾选【适用于 Linux 的 Windows 子系统】 1.2 Store中下载安装 在 Microsoft Store 中下载并安装需要的 Linux 发行版 2.不使用Store安装WSL 注&#xff1a;1.1也要…...

MongoDB 入门

1.1 数据库管理系统 在了解MongoDB之前需要先了解先数据库管理系统 1.1.1 什么是数据&#xff1f; 数据&#xff08;英语&#xff1a;data&#xff09;&#xff0c;是指未经过处理的原始记录。 一般而言&#xff0c;数据缺乏组织及分类&#xff0c;无法明确的表达事物代表的意…...

使用uni-app的uniCloud 云数据库入门:实现一个简单的增删改查

官方云数据库文档 前置步骤使用uni-app新建一个uniCloud项目 [外链图片转存失败,源站可能有防盗官方云数据库文档]!链机制,建议将()https://uniapp.dcloud.net.cn/uniCloud/hellodb.html)] 新建表 这里我加了几个测试字段 createTime、remark、money // 文档教程: https://un…...

【MATLAB第64期】【保姆级教程】基于MATLAB的SOBOL全局敏感性分析模型运用(含无目标函数,考虑代理模型)

【MATLAB第64期】【保姆级教程】基于MATLAB的SOBOL全局敏感性分析模型运用&#xff08;含无目标函数&#xff0c;考虑代理模型&#xff09; 版本更新&#xff1a; 2023/8/5&#xff1a; 1.因BP作为代理模型不稳定&#xff0c;经过测试&#xff0c;libsvm比rf /bp 效果稳定且精…...

Python web实战之Django用户认证详解

关键词&#xff1a; Python Web 开发、Django、用户认证、实战案例 概要 今天来探讨一下 Django 的用户认证吧&#xff01;在这篇文章中&#xff0c;我将为大家带来一些有关 Django 用户认证的最佳实践。 1. Django 用户认证 在开发 Web 应用程序时&#xff0c;用户认证是一个…...

每天五分钟机器学习:梯度下降算法和正规方程的比较

本文重点 梯度下降算法和正规方程是两种常用的机器学习算法,用于求解线性回归问题。它们各自有一些优点和缺点,下面将分别对它们进行详细的讨论。 区别 1. 梯度下降算法是一种迭代的优化算法,通过不断迭代调整参数来逼近最优解。它的基本思想是根据目标函数的梯度方向,沿…...

生信学院|08月18日《基于Flow Simulation的冷链运输产品案例》

课程主题&#xff1a;基于Flow Simulation的冷链运输产品案例 课程时间&#xff1a;2023年08月18日 14:00-14:30 主讲人&#xff1a;江流洋 生信科技 CAE专家 1、达索仿真方案介绍 2、项目介绍 3、案例分析 请安装腾讯会议客户端或APP&#xff0c;微信扫描海报中的二维码…...

不可错过的家装服务预约小程序商城开发指南

在当今社会&#xff0c;家装行业发展迅速&#xff0c;越来越多的人开始寻求专业的家装预约和咨询服务。对于不懂技术的新手来说&#xff0c;创建一个自己的家装预约咨询平台可能听起来很困难&#xff0c;但实际上通过一些第三方制作平台和工具&#xff0c;这个过程可以变得简单…...

任务 13、MidJourney种子激发极致创作,绘制震撼连贯画作

13.1 任务概述 通过本次实验任务&#xff0c;学员将深入了解Midjourney种子的概念和重要性&#xff0c;以及种子对生成图像的影响。他们将学会在Midjourney平台中设置种子值并调整其参数&#xff0c;以达到所需的效果。此外&#xff0c;任务还详细介绍了Midjourney V4.0版本中…...

IAR开发环境的安装、配置和新建STM32工程模板

IAR到环境配置到新建工程模板-以STM32为例 一、 简单介绍一下IAR软件1. IAR的安装&#xff08;1&#xff09; 下载IAR集成开发环境安装文件&#xff08;2&#xff09; 安装 2. 软件注册授权 二、IAR上手使用(基于STM32标准库新建工程)1、下载标准库文件2、在IAR新建工程&#x…...

FPGA优质开源项目 – PCIE通信

本文介绍一个FPGA开源项目&#xff1a;PCIE通信。该工程围绕Vivado软件中提供的PCIE通信IP核XDMA IP建立。Xilinx提供了XDMA的开源驱动程序&#xff0c;可在Windows系统或者Linux系统下使用&#xff0c;因此采用XDMA IP进行PCIE通信是比较简单直接的。 本文主要介绍一下XDMA I…...