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

Linux 网络套接字编程基础

端口号

我们在上一篇文章中以打电话的例子得出结论:在进行网络通信的时候,不是我们的两台机器在进行通信,本质上是应用层在进行通信。

为什么这么说呢?

  • 网络协议的下三层,解决的是数据安全可靠地发送到远端机器。这是通信的保障,并不是通信的目的。
  • 用户使用应用层的软件,完成数据的发送和接收。所以可以看作是应用层在进行通信。

而应用层的软件启动起来不就是一个进程嘛!

因此我们就又可以得出一个结论:两台机器进行通信只是一个手段,进程之间交换数据才是目的。因此日常网络通信的本质就是进程间通信!网络就是那个共享资源,通过网络协议栈来实现进程间通信。


我们用生活中使用微信的例子来引出端口号:
在这里插入图片描述

  • 左侧是客户端,在应用层他启动了一个微信的进程。当然在他的电脑里面肯定不止微信这一个进程。微信发送消息需要先将消息发送到服务器,当这个消息(报文)贯穿网络协议栈,到达了微信服务器的传输层,就会出现一个问题了:这个消息(有效载荷)应该交给上层的哪一个进程呢?
  • 在上一片文章中我们说过:每一层的协议都要在报头中提供将自己有效载荷交付给上层哪一个协议的能力。 对于传输层来讲,就是应该将有效载荷交给上层的哪一个进程呢?
  • 事实上在传输层的报头中有两个字段:源端口号,和目的端口号.
    1. 源端口号的作用:标识这个报文是对方机器的哪个进程发送给我的。
    2. 目的端口号:我收到的报文应该向上交给哪个进程。

来看看端口号的定义:端口号是用于标识主机上特定应用程序或服务的数字标识符。

如上图,假设服务器的传输层收到报文的目的端口号是 700,正好服务端微信这个进程绑定的端口号也是 700。传输层就知道这个报文是给微信这个进程的。


端口号是传输层协议的内容,端口号无论对于客户端还是服务器,都能唯一标识其主机上的一个进程。端口号常见性质:

  1. 端口号是一个 2 字节 16 个比特位的无符号整数。
  2. 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
  3. IP 地址 + 端口号能够表示网络上某一台主机的某一个进程。
  4. 一个端口号只能被一个进程占用。但是一个进程可以绑定多个端口号。

你肯定会想,之前学习系统的时候,我们不是学过进程的 pid 嘛,pid 在一台主机上也具有唯一性,为什么不直接将进程的 pid 作为端口号呢?

  • 首先,一台主机上有很多很多的进程,并不是所有的进程都需要进行网络通信啊,但是所有的进程都要有 pid。在定义端口号是为了能够将系统和网络区分开!
  • 其次,进程 pid 是系统的概念!端口号是网络的概念!如果进程 pid 和端口号共用一个数字,将来如果要对进程 pid 或者网络的端口号规则进行修改,这就会同时影响系统和网络两大模块!因此最主要的原因还是 为了实现网络和系统的解耦合!

不知到细心的 uu 有没有发现一个问题:客户端是怎么知道服务器的端口号的嘞?

  • 有一部分端口号是众所周知的,比如:http 服务用得端口号是80;https 使用的端口号是 443;xshell 中的远程登录使用的端口号是22;等等。
  • 在日常生活中我们使用抖音这样的软件,能够直接向抖音的服务器请求视频资源,显然在抖音这个软件的内部是内置了抖音服务器的端口号的!

服务器想要知道客户端的端口号就比较简单啦!因为服务器最开始是被动的一方,客户端第一次发起请求时,报文里面必定携带了自身的 ip 地址和端口号信息。

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

在游客这些前置知识,我们就能进行套接字编程啦!在上一篇文章中我们了解到传输层是在操作系统内部实现的,因此我们进行套接字编程的本质就是使用操作系统提供的接口哈!

套接字编程可以分为以下类别:

  1. 域间套接字编程:这是在同一个机器内进行通信,他是网络套接字编程的子集,用于本地通信的。了解即可。
  2. 原始套接字编程:这种套接字编程会绕过传输层协议,直接使用网络层和链路层的接口,一般用于编写网络工具。不需要了解。
  3. 网络套接字编程:这个才是我们进行重点学习的哈,网络套接字编程用于用户间进行网络通信。

udp 套接字编程

我们这里对 UDP (User Datagram Protocol) 有一个简单的认识就行,具体详细的讲解会放到后面。

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

udp 服务端编程

创建 udp 套接字

在这里插入图片描述

  • 参数一:表示协议家族。我们要进行网络套接字编程,这里一般填写 AF_INET 就行。AF_INET 表示网络通信,使用 ipv4 协议。AF_INET6 表示网络通信,使用 ipv6 协议。AF_UNIX 就是域间套接字通信用的,其他的什么还有很多,这里就不一一介绍了!
  • 参数二:表示套接字类型。常用的有 SOCK_STREAM:流式套接字,对应于TCP协议; SOCK_DGRAM:数据报套接字,对应于UDP协议等。我们根据需要选择适合的套接字类型。
  • 参数三:表示协议类型,通常设置为 0 表示使用默认协议。我们直接设置为默认的 0 就行了。
    1. 如果创建一个 TCP 套接字,可以将 protocol 参数设为 IPPROTO_TCP,而如果希望创建一个UDP 套接字,可以将 protocol 参数设为 IPPROTO_UDP
    2. 如果将 protocol 参数设为 0,系统会根据套接字类型和协议家族选择默认的协议。在需要明确使用 TCP 或 UDP 协议时,最好还是将 protocol 参数设为 IPPROTO_TCPIPPROTO_UDP,以确保协议选择的准确性。
  • 返回值:该函数返回值是一个整数,表示创建的套接字的文件描述符,如果创建失败则返回 -1 。套接字与文件描述符的具体关系等到后面会讲的,现在讲不清楚。

我们创建一个 UdpServer 的类,在类中顶一个成员变量用来接收 socket 函数返回的文件描述符!
在这里插入图片描述

套接字绑定

在这里插入图片描述

  • 参数一:表示要绑定的套接字的文件描述符,即通过 socket 函数创建的套接字。
  • 参数二:指向要绑定的本地地址结构的指针,通常是一个 sockaddr_in 结构体的指针,用于指定IP 地址和端口号。对于 IPv4 地址族,可以使用 sockaddr_in 结构体,对于IPv6地址族,可以使用sockaddr_in6 结构体。这个结构体用来初始化服务器绑定的 ip 地址和端口号。
  • 参数三:表示地址结构的长度,可以使用 sizeof 操作符来获取。就是外部创建的 sockaddr_in 结构体的大小。
  • 返回值:绑定成功返回 0;绑定失败返回 -1。

这里解释一下为什么参数的类型看上去和实际传入的类型不一样哈:
在这里插入图片描述
实际上不同种类的套接字通信需要的结构体是不一样的,struct sockaddr_in 是 ipv4 版本的网络通信要使用的结构体,struct sockaddr_un 是域间套接字通信要使用的结构体等等,为了使这些套接字通信共用一套网络接口,不得已弄出来一个 struct sockaddr 这个结构体。在内部会根据不同的类型做强制类型转换的!

那你就可能会问了:为啥不用 void * 指针呢?void * 不是能接收任意类型的指针变量嘛?事实上是网络接口出来的时候,C语言规范中还没有 void* 所以才设计出了这么一个结构体哈!


sockaddr_in 结构体中有三个字段需要我们填写:

  • sin_family:这个字段就填 socket 函数传入的那个协议家族,也就是 AF_INET
  • sin_port:其实就是服务端进程想要绑定的端口号啦!注意需要转换成网络字节序哦!
  • sin_addr:其实就是服务端进程想要绑定的 ip 地址啦!同样也要转换成网络字节序哦!

IPv4地址通常使用点分十进制表示法(如192.168.1.1)但在网络编程中,通常将其转换为32位的网络字节序整数表示,存储在s_addr字段中。

IPv4 地址一共是四个字节,每一个点作为一个字节的分割,我们在绑定 ip 地址的时候,都习惯这么写:“192.168.1.1” 但是我们的 sockaddr_in 结构体需要的是 32 为的整数。这就需要我们提供一种能够将字符串风格的 IP 地址转换为 32 整数的方法。


如何快速的将整数 IP 和 字符串 IP 进行相互转换:

4 字节整形 ip 转换成字符串 ip:

struct ip_struct
{uint8_t part1;uint8_t part2;uint8_t part3;uint8_t part4;
};std::string to_str(uint32_t ip)
{struct ip_struct* p = (struct ip_struct*)ip;std::string str_ip = "";str_ip += std::to_string(p->part1);str_ip += ".";str_ip += std::to_string(p->part2);str_ip += ".";str_ip += std::to_string(p->part3);str_ip += ".";str_ip += std::to_string(p->part4);str_ip += ".";return str_ip;
}

在上面的代码中我们定义了一个结构体,内含四个 1 字节的无符号整数变量,我们直接对 4 字节的整形 ip 地址强转为结构体指针,然后分别对每一部分转化成字符串就可以了!


字符串 ip 转化为 4 字节整形 ip:
这个就很好办了,我们只需要以 “.” 作为分隔符将字符串风格的 ip 地址截取成四部分,然后分别将其转换成整形,赋值到结构体中的每一个成员就行。


因为字符串 IP 和 整形 IP 的转换非常固定且比较容易,因此已经有库函数能帮助我们解决这个问题了,不需要我们自己动手写啦!


字符串 IP 转整形 IP
  1. inet_aton:这个函数可以将 C 风格的字符串的 ip 地址直接转换成 4 字节的整数 ip 地址,并且是网络字节序的。 结果通过输出型参数获取。在这里插入图片描述

    • 参数一:一个指向以点分十进制表示的 IPv4 地址的字符串的指针。
    • 参数二:输出型参数,转换成功的 sin_addr。跳转到 sockaddr_in 的定义:
      在这里插入图片描述
      我们可以看到 sin_addr 的类型就是 in_addr
      在这里插入图片描述
      in_addr 里面就只有一个 32 位的无符号整形:s_addr
    • 返回值: 转换成功返回转换成功的整形 ip。转换失败返回 0xffffffff
  2. inet_addr:这个函数可以将 C 风格的字符串的 ip 地址直接转换成 4 字节的整数 ip 地址,并且是网络字节序的。 结果通过返回值获取。
    在这里插入图片描述

  • 参数一:C 风格的字符串 ip 地址。
  • 返回值:网络字节序的,整形 ip 地址。注意返回值的类型哈,接收的话要用 sin_addr.in_addr_t 来接收。

整形 IP 转字符串 IP
  1. inet_ntoa:该函数可以将网络字节序的整形 ip 转换为 C 风格的字符串 IP。通过返回值来接收。
    在这里插入图片描述

    • 参数一:网络字节序的整形 IP。
    • 返回值:转换成功的 C 风格字符串的起始地址。

    这个函数有一个问题,因为转换成功的字符串是通过返回值来获取的,那么这个字符串转换成功之后是被保存在哪里的呢?根据文档的描述,转换成功的字符串是在静态区,也就是说无论你调用了多少次这个函数,转换成功的字符串都会存储在一份空间中,存在覆盖的问题。
    在《Unix 环境高级编程》一书中,说该函数不是线程安全的。
    因此我们更加推荐使用下面的这个函数,字节传入缓冲区来接收转换成功的结果。

  2. inet_ntop:用于网络字节序的整形的 IPv4 或 IPv6 地址转换为点分十进制表示的字符串。
    在这里插入图片描述

    • 参数一:一个整数,表示地址族(address family),可以是 AF_INET 或 AF_INET6,分别表示 IPv4 和 IPv6。
    • 参数二:指向整形的 IPv4 或 IPv6 地址的指针。
    • 参数三:一个指向用于存储点分十进制表示的 IP 地址的缓冲区的指针。
    • 参数四:缓冲区的大小,以字节为单位。
端口号转网络字节序

既然 IP 地址的转换都有库函数了,端口号怎么可以没有呢?
在这里插入图片描述

  • 第一个函数将 32 位的端口号由主机序列转换成网络序列。
  • 第二个函数将 16 位的端口号由主机序列转换成网络序列。
  • 第三个函数将 32 位的端口号由网络序列转换成主机序列。
  • 第四个函数将 16 位的端口号由网络序列转换成主机序列。

怎么记?很简单:

  • h:host 主机
  • n:net 网络
  • s:short 短 16 位
  • l:long 长 32 位

那 bind 函数该怎么写呢?

在这里插入图片描述
这里有一个问题就是:服务端的 IP 地址应该绑定哪一个呢?我们绑定云服务器的公网 IP 试一试:我们为 UdpServer 类添加构造函数,调用一下 Init 函数:
在这里插入图片描述
我们发现报错:bind error。这是为什么呢?

  • 服务器进制绑定公网 IP,因为有的服务器有多张网卡,那么就有多个 ip 地址,如果绑定了一个固定 ip 地址,就收不到另一个 ip 地址本应该接收到的消息。通常我们喜欢将服务器绑定的 ip 设置为:0.0.0.0
  • 在套接字编程中,当服务端绑定 IP 地址为 0.0.0.0 时,表示服务端将监听所有可用的网络接口(网卡)上的连接请求。具体而言,0.0.0.0 是一个特殊的 IP 地址,通常被称为通配符地址或者未指定地址。
  • 当服务端绑定到 0.0.0.0 地址时,它将会接受来自任何网络接口(包括本地网络接口和公网接口)的连接请求。这意味着,无论服务端位于本地网络还是公网,都能够接受到该服务端绑定的端口上的连接请求。
  • 绑定到 0.0.0.0 地址的服务端在多网卡或多 IP 地址的环境下非常有用,因为它能够接受来自任何网络接口的连接请求,无需为每个网络接口分别设置监听。
  • 需要注意的是,绑定到 0.0.0.0 地址的服务端可能会接受来自任何地方的连接请求,因此在安全性方面需要谨慎处理。

服务器绑定 0.0.0.0 这个 ip 地址就简单啦:直接将 sin_addr 设置为 0 就行啦!或者将 sin_addr.s_addr 设置为一个宏:INADDR_ANY。一个效果哈!

收发消息

我们现在要实现的服务器干什么事情呢?就将用户发来的字符串原封不动地发回去哈!

接收消息

在这里插入图片描述

  • sockfd:是一个已经创建的套接字描述符,通过 socket() 函数创建。
  • buf:是一个指向存放接收数据的缓冲区的指针。
  • len:是接收缓冲区的大小,即缓冲区 buf 的长度。
  • flags:是一组标志位,用于控制接收操作的行为。常用的标志位包括 MSG_DONTWAIT 表示非阻塞模式接收,MSG_WAITALL 表示要求等待所有数据到达等。我们这里先不管,直接设置为 0 就行,具体等到后面讲协议的时候再说。填写 0 默认阻塞接收。
  • src_addr:是一个指向 struct sockaddr 类型的指针,用于存放发送方的地址信息。一般用于获取发送者的 ip 和端口号信息以及后续给他发送消息。
  • addrlen:是一个指向 socklen_t 类型的指针,用于表示 src_addr 缓冲区的大小。
  • 返回值:成功返回实际收到了多少字节的数据,失败返回 -1。返回值为 0 代表对端关闭连接。

在这里插入图片描述

发送消息

在这里插入图片描述

  • sockfd:是一个已经创建的套接字描述符,通过 socket() 函数创建。
  • buf:是一个指向包含待发送数据的缓冲区的指针。
  • len:是待发送数据的字节数,即缓冲区 buf 的长度。
  • flags:是一组标志位,用于控制发送操作的行为。常用的标志位包括 MSG_DONTWAIT 表示非阻塞模式发送,MSG_CONFIRM 表示发送后要求对端返回确认信息等。这里同样我们直接设置为 0 就行,使用默认的发送方式。
  • dest_addr:是一个指向 struct sockaddr 类型的指针,表示目标地址的信息。
  • addrlen:是目标地址信息的长度。
  • 返回值:如果返回值为正数,表示成功发送了数据,并且返回值表示实际发送的字节数。
    如果返回值为 -1,表示发生了错误。

Udp 服务端完整代码

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <stdlib.h>
#include <errno.h>enum
{SOCK_CREATE_ERR = 1,BIND_ERR,RECV_ERR,SEND_ERR
};class UdpServer
{
public:UdpServer(uint16_t port) : _port(port) {}void Init(){//创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(_sockfd < 0){std::cout << "socket create error" << std::endl;exit(SOCK_CREATE_ERR);}std::cout << "socket create success" << std::endl;struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);// inet_aton(_ip.c_str(), &(local.sin_addr));local.sin_addr.s_addr = INADDR_ANY;//绑定 ip 地址和端口号if(bind(_sockfd,(sockaddr*)(&local), sizeof(local)) < 0){std::cout << "bind error" << std::endl;exit(BIND_ERR);}std::cout << "bind success" << std::endl;}void Start(){char recvbuff[1024];while(true){struct sockaddr_in sender;bzero(&sender, sizeof(sender)); //用 memset 函数也行socklen_t len = sizeof(sender);int n = recvfrom(_sockfd, recvbuff, sizeof(recvbuff) - 1, 0, (sockaddr*)(&sender), &len);if(n < 0){std::cout << "recv message error" << std::endl;exit(RECV_ERR);}//当作字符串处理recvbuff[n] = 0;char sender_ip[48];//获取 发送者的 ip 地址inet_ntop(AF_INET, (void*)(&sender.sin_addr.s_addr), sender_ip, sizeof(sender_ip));std::cout << sender_ip << ":";// 获取发送者的 端口号uint16_t sender_port = ntohs(sender.sin_port);std::cout << sender_port << " ";std::cout << "client say: " << recvbuff << std::endl;std::string message = "server echo: " + std::string(recvbuff);n = sendto(_sockfd, message.c_str(), message.size(), 0, (sockaddr*)(&sender), sizeof(sender));if(n < 0){std::cout << "send message error" << std::endl;exit(SEND_ERR);}}}private:int _sockfd;uint16_t _port;
};

Main.cc 代码:

Udp 客户端编程

客户端的代码和服务端的代码大差不差,只是说客户端不用显示绑定端口号和 ip 地址了!为什么呢?

  • 首先必须明确的观点是:客户端一定要绑定 ip 地址和端口号,只不过不需要我们程序员手动绑定,而是由操作系统随机选择一个端口号来绑定。
  • 其实让客户端显示的绑定端口号本身就不合理,因为一个端口号不能被两个进程绑定。假设两个进程绑定了同一个端口号,那么就一定有一个进程启动不起来!
  • 其实客户端的端口号是多少并不重要,只要能保证该端口号在主机上的唯一想那个就可以啦!

既然是操作系统帮我绑定 ip 地址和端口号,那绑定的时间是多少呢?
其实是客户端第一次发送数据的时候哈!

Udp 客户端完整代码

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <errno.h>void Usage()
{std::cout << "./udpclient serverip serverport" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage();exit(1);}std::string ip = argv[1];int port = std::stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0)exit(1);struct sockaddr_in dst;dst.sin_port = htons(port); // 将命令行传入的端口号转成网络字节序列dst.sin_family = AF_INET; // 使用 ipv4// dst.sin_addr.s_addr = inet_addr(ip.c_str());int n = inet_aton(ip.c_str(), &(dst.sin_addr)); // 将命令行输入的 ip 地址转换为网络字节序列if(n < 0 ) exit(1);char buff[1024]; // 这个数组用来接收服务端发送给自己的数据std::string message; // 这个是客户端发送给服务端的数据while (true){std::cout << "Please Enter: "; // 提示用户输入getline(std::cin, message); // 用户输入,用作发送给服务端的数据// 向服务端发送数据sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)(&dst), sizeof(dst));// 这就是那个结构体,作为 recvfrom 函数的输出型参数,可以知道谁给你发送的消息struct sockaddr_in tmp;socklen_t len = sizeof(tmp);int n = recvfrom(sockfd, buff, sizeof(buff) - 1, 0 ,(sockaddr*)(&tmp), &len);buff[n] = 0; //解析成为字符串std::cout << buff << std::endl; // 将服务端发送给客户端的数据打印出来}return 0;
}

Udp 通信测试

在测试之前记得开放一下我们要测试的端口号哈:协议要选择 UDP 协议哦!
在这里插入图片描述
在这里插入图片描述
我有两台云服务器:
左侧是客户端,ip 是:47.180.251.0,向服务器发送消息。
右侧是服务端,ip 是:47.76.92.32 客户端绑定这个 ip。
可以看到服务端能够收到客户端(47.180.251.0) 发来的数据,并且也能正确将数据发送到客户端。

这个代码仅支持一个客户端和服务端进行通信,这显然是不合理的!因此你可以在此基础上增加多进程版,多线程版,线程池版!让其能处理多个客户端的通信!

TCP 套接字编程

我们来简单了解一下 TCP 协议:
让我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

其实 TCP 套接字编程和 UDP 套接字编程真的差不多。只是个别填的参数不同,还有就是读写的方式不同,TCP 通信我想等到讲解自定义协议和序列化反序列化的时候讲!!

知识点总结:

  • 认识并理解端口号
  • 熟练掌握 UDP / TCP 套接字编程相关接口

相关文章:

  • 人工智能在未来的优势
  • SpringBoot使用log4j2将日志记录到文件及自定义数据库
  • Django快速入门
  • Kafka 技术指南:使用、特性、一致性保证与 Golang 中间件应用(下)
  • 【茶话数据结构】查找最短路径——Dijkstra算法详解(保姆式详细图解,步步紧逼,保你学会)
  • 【目标检测经典算法】R-CNN、Fast R-CNN和Faster R-CNN详解系列二:Fast R-CNN图文详解
  • 走进网络世界 了解一些基础知识
  • rabbitmq-spring-boot-start配置使用手册
  • 数字孪生10个技术栈:数据清洗-数据的洗衣机
  • Qt+FFmpeg+opengl从零制作视频播放器-15.音视频一些知识
  • 鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Toggle)
  • VS 调试Hololens 2工程报错 有未经处理的异常: Microsoft C++ 异常:
  • 2115. 从给定原材料中找到所有可以做出的菜
  • 垃圾回收器介绍
  • FDU 2020 | 1. 食堂打饭
  • Android 架构优化~MVP 架构改造
  • axios 和 cookie 的那些事
  •  D - 粉碎叛乱F - 其他起义
  • express.js的介绍及使用
  • Fundebug计费标准解释:事件数是如何定义的?
  • JAVA 学习IO流
  • Java超时控制的实现
  • Joomla 2.x, 3.x useful code cheatsheet
  • PHP的类修饰符与访问修饰符
  • React的组件模式
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • Vue--数据传输
  • vue中实现单选
  • 百度小程序遇到的问题
  • 不上全站https的网站你们就等着被恶心死吧
  • 程序员该如何有效的找工作?
  • 仿天猫超市收藏抛物线动画工具库
  • 汉诺塔算法
  • 欢迎参加第二届中国游戏开发者大会
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 力扣(LeetCode)22
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 腾讯大梁:DevOps最后一棒,有效构建海量运营的持续反馈能力
  • 详解NodeJs流之一
  • hi-nginx-1.3.4编译安装
  • 选择阿里云数据库HBase版十大理由
  • #define,static,const,三种常量的区别
  • #if #elif #endif
  • (1)SpringCloud 整合Python
  • (2)MFC+openGL单文档框架glFrame
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (超简单)构建高可用网络应用:使用Nginx进行负载均衡与健康检查
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (四)Android布局类型(线性布局LinearLayout)
  • (转载)OpenStack Hacker养成指南
  • .net 发送邮件
  • .net 使用ajax控件后如何调用前端脚本