Linux 高性能服务器网络编程

  • Linux网络编程基础API
    • Socket 地址API
      • 通用socket 地址
      • 专用Sokect地址
      • IP地址转换函数
      • 创建socket(socket)
      • 命名(绑定)socket(bind)
      • 监听socket(listen)
      • 接收连接accpet
      • 发起连接(connect)
      • 关闭连接
      • 读写操作
      • 一个基本的C/S程序
      • 地址信息函数
      • SOCKET选项(*)
  • 高级IO函数
    • pipe()
    • dup() / dup2()
    • readv() / writev()
    • sendfile()
    • mmap() / munmap()
    • splice()
    • fcntl() 重点

参考自《高性能服务器编程》,主要用于学习网络编程模块

Linux网络编程基础API

Socket 地址API

(1)网络字节序:大端字节序(低位存放高地址)
(2)主机字节序:小端字节序 (低位存放低地址)

通用socket 地址

#include <bits/socket.h>

struct sockaddr
{sa_family_t sa_family;char sa_data[14];
}

sa_family 地址族一般与协议族相对应

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地域协议族
PF_INET AF_INET TCP/IPv4协议族
PF_INET6 AF_INET6 TCP/IPv6协议族

专用Sokect地址

注:此处只学习IPV4/IPV6协议族,对UNIX本地族域不做解释
// 這些都不建議使用!請使用 inet_pton() 或 inet_ntop() 取代!

#include <sys/un.h>
struct sockaddr_in
{sa_family_t sin_family;    /*地址族: AF_UNIX*/u_int16_t sin_port;  /*端口号, 要用网络字节序*/struct  in_addr sin_addr;    /*IPV4地址结构体*/
};
struct  in_addr /*IPV4地址结构体*/
{u_int32_t s_addr;
};struct  sockaddr_in6
{sa_family_t sin6_family;   /*地址族: AF_UNIX*/u_int16_t sin6_port; /*端口号, 要用网络字节序*/u_int32_t sin6_flowinfo; /*流信息,应该设置0*/struct  in6_addr sin_addr;  /*IPV4地址结构体*/u_int32_t sin6_scope_id;
};struct  in_addr   /*IPV4地址结构体*/
{unsigned char sa_addr[16]
};

IP地址转换函数

#include <arpa/inet.h>
/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :网络字节序整数表示的IPv4地址 失败 :INADDR_NONE
*@pram strptr : 点分十进制字符串表示的IPV4
*/
in_addr_t inet_addr( const char *strptr);/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :1 失败 :0
*@pram cp : 点分十进制字符串表示的IPV4,inp:转换结果存于网络字节序结果
*/
int inet_aton( const char *cp, struct in_addr *inp);
/*description :网络字节序整数表示的IPv4地址转换成点分十进制字符串表示的IPV4
*return 成功 :成点分十进制字符串表示的IPV4 失败 NULL
*@pram in : 点分十进制字符串表示的IPV4
*/
注:此函数是不可重入函数,返回的值指向该静态内存
char *inet_ntoa( struct in_addr in);

*重点

#include <arpa/inet.h>
/*description :网络字节序转换为主机字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6
*       dst : 转换结果存于网络字节序结果, size:INET_ADDRSTRLEN(16)/INET6_ADDRSTRLEN(46)
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/*description :主机字节序转换成网络字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6 dst : 转换结果存于网络字节序结果
*/
int inet_pton(int af, const char *src, void *dst);

创建socket(socket)

#include <sys/types.h>
#include <sys/socket.h>
/*description :创建socket文件
*return 成功:socket文件描述符 失败: 返回-1并且设置errno
*@pram domain(底层协议) : PF_INET / PF_INET6;
*type(服务类型) :SOCK_STREAM(TCP流服务)/ SOCK_DGRAM(UDP数据报) / SOCK_NONBLOCK(设置非阻塞) / SOCK_CLOEXEC(用fork创建子进程时在子进程中关闭该socket)
*protocol : 0
*/
int socket(int domain, int type, int protocol);

命名(绑定)socket(bind)

用于绑定具体的socket和port

#include <sys/types.h>
#include <sys/socket.h>
/*description :绑定socket,将my_addr所指的socket地址分配给为命名的sockfd 文件描述符,addrlen参数指出该socket地址的长度
*return 成功:0 失败: 返回-1并且设置errno
*       常见errno: EACCES : 普通用户绑定到0~1023端口上/EADDREINUSE:TIME_WAIT状态的socket地址
*@pram sockfd 文件描述符; my_addr : sockaddr; addrlen : 地址长度
*/
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

监听socket(listen)

/*description :监听指定socket文件描述符
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd 文件描述符;
*backlog : 指定所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限
*内核2.23之后,backlog只表示完全连接状态(ESTABLISHED)的上限,而半连接直接由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义
*backlog参数典型值为 5
*/
int listen(int sockfd, int backlog);

注:完整连接一般为(backlog + 1),不同的系统略有不同不过监听队列中的完整的连接的上限比bakclog略大

接收连接accpet

#include <sys/types.h>
#include <sys/socket.h>
/*description :从listen监听队列中接收一个连接
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd : listen Socket文件描述符;addr : 获取接收连接远端的socket地址 addrlen:长度
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注:accpet函数只是往监听队列取出连接,不管连接处于什么状态,更加不关心网络变化

发起连接(connect)

#include <sys/types.h>
#include <sys/socket.h>
/*description :客户端主动向服务器建立连接
*return 成功:0 失败: 返回-1并且设置errno
*ECONNREFUSED 目的端口不存在   ETIMEDOUT : 连接超时
*@pram sockfd : 返回与服务端通信的socket;serv_addr : 服务端地址 addrlen:长度
*/
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

关闭连接

#include <unistd.h>
int close( int fd);注:并非总是关闭一个连接,而是将fd 的引用计数减1,只有当fd的引用计数为0时才真正的关闭连接,多进程程序中一次fork会将父进程中打开的socket + 1,因此在父子进程中都要对该socket执行close才能将连接关闭
/*description :关闭一方连接
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd : 需要关闭的socket
*how :如下有详解
*/
int shutdown(int sockfd, int how)

how参数可选值

可选值 含义
SHUT_RD(0) 关闭sockfd的读操作,应用程序不能从sockfd执行读操作,并且把该socket接收缓冲区的数据丢弃
SHUT_WR (1) 关闭sockfd的写操作,sockfd的发送缓冲区中的数据会在真正的关闭连接之前全部发出去,应用程序不在对sock执行写操作。这种情况连接处于半关闭状态
SHUT_RDWR(2) 关闭sockfd的读写操作

读写操作

#include <sys/types.h>
#include <sys/socket.h>
/*description :从sockfd写数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t send(int sockfd, const void *buf, size_t len, int flags);/*description :从sockfd接收数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;当返回 0 时对方关闭了连接, 出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t recv(int sockfd, void *buf, size_t len, int flags);

一个基本的C/S程序

client

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>extern void sig_proccess(int signo);
extern void sig_pipe(int signo);
static int s;
void sig_proccess_client(int signo)
{printf("Catch a exit signal\n");close(s);exit(0);
}#define PORT 8888  /* 侦听端口地址 */
int main(int argc, char *argv[])
{struct sockaddr_in server_addr;    /* 服务器地址结构 */int err;/* 返回值 */signal(SIGINT, sig_proccess);signal(SIGPIPE, sig_pipe);/* 建立一个流式套接字 */s = socket(AF_INET, SOCK_STREAM, 0);if(s < 0){/* 出错 */printf("socket error\n");return -1; }   /* 设置服务器地址 */bzero(&server_addr, sizeof(server_addr));      /* 清0 */server_addr.sin_family = AF_INET;              /* 协议族 */server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */server_addr.sin_port = htons(PORT);                /* 服务器端口 *//* 将用户输入的字符串类型的IP地址转为整型 */inet_pton(AF_INET, argv[1], &server_addr.sin_addr);    /* 连接服务器 */connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));process_conn_client(s);  /* 客户端处理过程 */close(s);  /* 关闭连接 */
}

server:

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>extern void sig_proccess(int signo);#define PORT 8888      /* 侦听端口地址 */
#define BACKLOG 2       /* 侦听队列长度 */
int main(int argc, char *argv[])
{int ss,sc;     /* ss为服务器的socket描述符,sc为客户端的socket描述符 */struct sockaddr_in server_addr; /* 服务器地址结构 */struct sockaddr_in client_addr; /* 客户端地址结构 */int err;   /* 返回值 */pid_t pid; /* 分叉的进行id */signal(SIGINT, sig_proccess);signal(SIGPIPE, sig_proccess);/* 建立一个流式套接字 */ss = socket(AF_INET, SOCK_STREAM, 0);if(ss < 0){/* 出错 */printf("socket error\n");return -1;    }   /* 设置服务器地址 */bzero(&server_addr, sizeof(server_addr));  /* 清0 */server_addr.sin_family = AF_INET;          /* 协议族 */server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */server_addr.sin_port = htons(PORT);            /* 服务器端口 *//* 绑定地址结构到套接字描述符 */err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));if(err < 0){/* 出错 */printf("bind error\n");return -1;   }/* 设置侦听 */err = listen(ss, BACKLOG);if(err < 0){/* 出错 */printf("listen error\n");return -1;  }/* 主循环过程 */for(;;) {int addrlen = sizeof(struct sockaddr);/* 接收客户端连接 */sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);if(sc < 0){      /* 出错 */continue;   /* 结束本次循环 */}   /* 建立一个新的进程处理到来的连接 */pid = fork();     /* 分叉进程 */if( pid == 0 ){     /* 子进程中 */close(ss);        /* 在子进程中关闭服务器的侦听 */process_conn_server(sc);/* 处理连接 */}else{close(sc);       /* 在父进程中关闭客户端的连接 */}}
}

地址信息函数

#include <sys/unistd.h>/*description :获取hostName
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int gethostname(char *name, size_t len);
#include <sys/socket.h>
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getsockname(int sockfd, struct sockaddr *address, socklen_t *address_len);
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getpreename(int sockfd, struct sockaddr *address, socklen_t *address_len);

SOCKET选项(*)

#include <sys/types.h>
#include <sys/socket.h>int getsockopt(int sockfd, int level, int optname, void *optval,socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval,socklen_t optlen);

注:服务端对listen socket设置,accpet返回后会继承这些opt选项
客户端对connect设置,因为对于客户端connect结束就已经建立了连接

SO_REUSEADDR : 即使sock处于TIME_WAIT状态,只要设置了SO_RCVBUF也能立即重用
也可以修改内核参数:/proc/sys/net/ipv4/tcp_rw_recycle,快速回收关闭socket,从而tcp连接不进入TIME_WAIT的状态

SO_RECVBUF/SO_SNDBUF:用setsockopt设置时,系统一般会将设置的值加倍,但是需要在一定范围内,查看 /proc/sys/net/ipv4/tcp_wmem 和 /proc/sys/net/ipv4/tcp_rmem,目的是为了确保TCP连接有足够的空闲缓冲区来处理拥塞

SO_RCVLOWAT 和SO_SNDLOWAT选项

作用:被IO复用系统调用(epoll)用来判断socket是否可读可写,当读缓冲区中可读数据大于读低水位时,到对应的socket读取数据;当写缓冲区中可读数据大于写低水位时,到对应的socket写入数据

SO_LINGER
作用:用于控制clsoe系统调用在关闭TCP连接时的行为。默认情况下,close调用之后立即将TCP发送缓冲区的数据发送给对端

#include <sys/socket.h>
struct linger
{int l_onoff  /*开启(非0)关闭(0)*/int l_linger /*滞留时间*/
};

设置不同的变量在close之后会产生三种不同的行为:
(1)l_onoff = 0,默认关闭socket
(2)l_onoff != 0,调用close立即返回,丢弃发送缓冲区的数据,发送一个RST报文,因此这是一个终止异常连接的方法
(3)l_onoff != 0, l_linger > 0;close行为取决与缓冲区是否有数据,socket是否阻塞
对于阻塞socket,close等待一段时间,直到缓冲区数据发送并且确认,如果这段时间TCP模块没有发送完数据并且得到对方确认那么close系统调用返回-1,errno = EWOWLDBLOCK

对于非阻塞,close立即返回,根据返回值和errno的值来判断数据是否发送完毕

注:

#include <netdb.h>
//查看error的字符串形式
const char *gai_strerror( int error);

高级IO函数

pipe()

创建普通管道用于进程之间的通信

#include <unistd.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int pipe( int fd[2]);

socket基础api提供了快速创建双向管道的

#include <sys/types.h>
#include <sys/socket.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram 与socket()参数相同,不过domain 只能用AF_UNIX
*/int socketpair(int domain, int type, int protocol, int fd[2])

dup() / dup2()

#include <unistd.h>
/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram
*/
int dup( int file_descriptor);/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram
*/
int dup( int file_descriptor, int file_descriptor_two)

注:提供dup和dup2产生的新描述符并不继承原来文件描述符的属性,比如close-on-exec 和non-blocking
同时也是CGI服务器的基础

readv() / writev()

#include <sys/uio.h>
/*description :将数据从文件描述符读到分散的内存块即分散读/将分散内存的数据写入文件描述符,相当于简化的recvmsg/sendmsg
*return :成功:返回读出/写入的fd字节数 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram struct iovec
*{
*    // Starting address (内存起始地址)
*       void  *iov_base;
*     // Number of bytes to transfer(这块内存长度)
*       size_t iov_len;
*}
*/
ssize_t readv( int fd, const struct iovec *vector, int count)
ssize_t writev( int fd, const struct iovec *vector, int count)

sendfile()

在两个文件描述符之间直接传递数据(有内核操作), 避免了内核与用户缓冲区的数据拷贝,sendfile( ) 系统调用利用 DMA 将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中。接下来,DMA 将数据从内核 socket 缓冲区中拷贝到网卡中去。如果在用户调用 sendfile ( ) 系统调用进行数据传输的过程中有其他进程截断了该文件,那么 sendfile ( ) 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。

#include <sys/sendfile.h>
/*description :文件描述符之间传递数据
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram out_fd : 待写入内容,必须是个socket
*       in_fd : 待读入内容,必须指向一个文件(支持mmap函数的文件描述符),不能是socket
*       offset : 指定in_fd的那个位置开始读
*       count : 参数的字节数
*/
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

mmap() / munmap()

用于申请一段共享内存(虚拟内存),可以作为IPC 的share memory 也可以文件之间映射到内存
#include <sys/mman.h>

/*description :申请一段内存空间
* return : 成功:返回指向目标区域的指针 失败:返回MAP_FAILED((void *)-1),并且设置errno
*@pram start : 指定内存的起始地址   length : 内存长度
*   prot : 内存段的访问权限,如下
*   PROT_READ : 内存端可读 PROT_WRITE : 内存段可写
*   PROT_EXEC : 内存段可执行   PROT_NONE :内存段不可访问
*/
void *mmap( void *start, size_t length, int prot, int flags, int fd, off_t off_t);
/*description :释放一段内存空间
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno
*@pram start : 指定内存的起始地址   length : 内存长度
*/
int munmap( void *start, size_t length);

flag参数:

splice()

用于两个文件描述符之间数据移动,也是零拷贝技术

#include <fcntl.h>
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);

参数意义:

fdin参数:待读取数据的文件描述符。
offin参数:指示从输入数据的何处开始读取,为NULL表示从当前位置。如果fdin是一个管道描述符,则offin必须为NULL。
fdout参数:待写入数据的文件描述符。
offout参数:同offin,不过用于输出数据。
len参数:指定移动数据的长度。
flags参数:表示控制数据如何移动,可以为以下值的按位或:

SPLICE_F_MOVE:按整页内存移动数据,存在bug,自内核2.6.21后,实际上没有效果。
SPLICE_F_NONBLOCK:非阻塞splice操作,实际会受文件描述符本身阻塞状态影响。
SPLICE_F_MORE:提示内核:后续splice将调用更多数据。
SPLICE_F_GIFT:对splice没有效果。
fdin和fdout必须至少有一个是管道文件描述符。

返回值:

返回值>0:表示移动的字节数。
返回0:表示没有数据可以移动,如果从管道中读,表示管道中没有被写入数据。
返回-1;表示失败,并设置errno。

errno值如下:

EBADF:描述符有错。
EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
ENOMEM:内存不够。
ESPIPE:某个参数是管道描述符,但其偏移不是NULL。

fcntl() 重点

提供对文件描述符的各种控制操作,另外一个类似的系统调用ioctl,而且ioctl比fcntl执行的范围更加广

#include <fcntl.h>
int fcntl(int fd, int cmd, ...);

fd:被操作的文件描述符

cmd及其第三个定义如下图

一般网络编程需要将文件描述符设为非阻塞的

int setnonblocking( int fd )
{int old_option = fcntl( fd, F_GETFL );int new_option = old_option | O_NONBLOCK;fcntl( fd, F_SETFL, new_option );return old_option;
}

Linux 高性能服务器网络编程(一)相关推荐

  1. Linux后端服务器网络编程之线程模型丨reactor模型详解

    前言   上一篇文章<后端服务器网络编程之 IO 模型>中讲到服务器端高性能网络编程的核心在于架构,而架构的核心在于进程/线程模型的选择.本文将主要介绍传统的和目前流行的进程/线程模型,在 ...

  2. Linux 高性能服务器开发笔记:Reactor 模型定时器 | 网络编程定时器

    本文主要根据游双书本 Linux 高性能服务器开发 学习分析 linux 网络编程常用到的定时器模型,配备详细理解和分析,同时分析了 Linux 内核中定时器的低精度时间轮和高精度定时器实现思路还有 ...

  3. 《Linux高性能服务器编程》——导读

    前 言 为什么要写这本书 目前国内计算机书籍的一个明显弊病就是内容宽泛而空洞.很多书籍长篇大论,恨不得囊括所有最新的技术,但连一个最基本的技术细节也无法解释清楚.有些书籍给读者展现的是网络上随处可见的 ...

  4. 《Linux高性能服务器编程》学习笔记

    <Linux高性能服务器编程>学习笔记 Linux高性能服务器编程 TCP/IP协议族 TCP/IP协议族体系结构以及主要协议 数据链路层 网络层 传输层 应用层 封装 分用 测试网络 A ...

  5. Linux高性能服务器编程——书籍阅读笔记

    目录 前言 正文 第一章 1. 零拷贝函数 2. TCP/IP协议族 3. OSPF 4. ARP协议 5. RARP 6. ICMP协议 7. TCP协议 8. UDP协议 9. 封装 第四章 TC ...

  6. Linux 高性能服务器编程——多线程编程

    问题聚焦:     在简单地介绍线程的基本知识之后,主要讨论三个方面的内容:     1 创建线程和结束线程:     2 读取和设置线程属性:     3 线程同步方式:POSIX信号量,互斥锁和条 ...

  7. 【Todo】【读书笔记】Linux高性能服务器编程

    在读 /Users/baidu/Documents/Data/Interview/服务器-检索端/<Linux高性能服务器编程.pdf> 其实之前读过,要面试了,需要温习. P260 So ...

  8. 【Linux】socket网络编程之服务器与客户端的数据交互

    [Linux]socket网络编程之服务器与客户端的数据交互 前言参考 argc !=2是什么意思 inet pton函数 对stdin,stdout 和STDOUT_FILENO,STDIN_FIL ...

  9. linux应用之--网络编程

    linux网络编程 一:网络参考模型参考模型,如下图所示: 二:TCP/IP协议 TCP协议(传输控制协议)和UDP协议(用户数据包协议)是工作在传输层的.  其中TCP协议是面向连接的,UDP是面向 ...

最新文章

  1. *用C#创建Windows服务(Windows Services)
  2. 工业界 vs. 学术界: 一个年轻员工的视角
  3. 01_关于TensorFlow、什么是数据流图(Data Flow Graph)、TensorFlow的特征、谁可以使用Tensorflow、为啥Google要开源这个神器?
  4. javascript的执行上下文
  5. iOS10 资料汇总:值得回看的 10 篇 iOS 热文
  6. 了解Linux操作系统发展阶段
  7. 安全攻击层出不穷,绿盟科技“智慧安全 3.0”安全防护再升级
  8. python 列表为空_如果列表为空,则Python返回False
  9. abap object-oriented–使用事件
  10. chattr与lsattr命令
  11. ce游戏逆向修改之扫雷
  12. Netty权威指南2.1BIO通信Demo代码
  13. 记一次刷票过程的感想
  14. 港科夜闻|香港科技大学(广州)拟获批首个省级重点实验室
  15. Three.js入门指南
  16. 极路由X(C526A)刷Openwrt 18.06固件
  17. Monkey自动化测试
  18. 国行Android手机使用google全套GMS服务小结
  19. jenkins windows 20008 R2 msi 工作目录迁移
  20. U-Mail邮件中继功能使用方法

热门文章

  1. python微信图片dat转码
  2. 云原生 · DevOps`01 | 光速初识DevOps
  3. shells - 有效登录 shell 的路径名
  4. 房地产基础知识!!!
  5. 你小子,又在偷偷学this指向
  6. 杨国福冲刺香港上市:加盟店风险事件频现,杨氏家族已“套现”1亿元
  7. discuz mysql 类_Discuz论坛中的的MySQL类解析
  8. 读薄《深入理解 Java 虚拟机》 JVM 的内存分配策略
  9. 金融业移动管理驾驶舱产品功能介绍
  10. 2020大二下期学期计划