NIO 源码分析(02-1) BIO 源码分析

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

一、BIO 最简使用姿势

(1) JDK BIO 启动服务典型场景

// 1. 绑定端口
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress((InetAddress) null, PROT), BACKLOG); while (true) {
// 2. 获取客户端请求的Socket,没有请求就阻塞
Socket socket = serverSocket.accept();
// 3. 开启一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
} // 绑定端口,开启服务
public void bind(SocketAddress endpoint, int backlog) throws IOException {
getImpl().bind(epoint.getAddress(), epoint.getPort());
getImpl().listen(backlog);
}

ok,代码已经完成!!!下面我们和 Linux 下的网络编程进行对比。

(2) Linux BIO 启动服务典型场景

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr);
int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);

对比 Linux 上网络编程,我们会发现 JDK Socket 的编程逻辑是一模一样的。实时上也是这样,JDK 网络编程也没有做很多事,主要还是调用了 Linux 相关的函数。唯一的不同是 Linux 是面向过程程序,socket 函数返回的是一个句柄,bind 和 listen 都是对这个句柄的操作;而 JDK 是面向对象编程,new ServerSocket() 返回了一个对象,我们可以调用这个 serverSocket 对象的各种方法。

Linux NIO 系列(02) 阻塞式 IO 网络编程

二、ServerSocket 源码分析

JDK 为我们提供了 ServerSocket 类作为服务端套接字的实现,通过它可以让主机监听某个端口而接收其他端的请求,处理完后还可以对请求端做出响应。它的内部真正实现是通过 SocketImpl 类来实现的,它提供了工厂模式,所以如果自己想要其他的实现也可以通过工厂模式来改变的。默认的实现类是 SocksSocketImpl,ServerSocket 和 Socket 只是一个门面模式。

2.1 相关类图

前面说到 ServerSocket 类真正的实现是通过 SocketImpl 类,于是可以看到它使用了 SocketImpl 类,但由于 windows 和 unix-like 系统有差异,而 windows 不同的版本也需要做不同的处理,所以两类系统的类不尽相同。

说明:

  1. SocketImpl 类实现了 SocketOptions 接口,接着还派生出了一系列的子类,其中 AbstractPlainSocketImpl 是原始套接字的实现的一些抽象,而 PlainSocketImpl 类是一个代理类。

  2. windows 下 PlainSocketImpl 代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 两种不同实现。存在两种实现的原因是一个用于处理 Windows Vista 以下的版本,另一个用于处理 Windows Vista 及以上的版本。

  3. unix-like 不存在版本的问题,所以它直接由 PlainSocketImpl 类实现。

  4. 这两类操作系统都还存在一个 SocksSocketImpl 类,它其实主要是实现了防火墙安全会话转换协议,包括 SOCKS V4 和 V5 。

根据上面可以看到其实对于不同系统就是需要做差异处理,基本都是大同小异,下面涉及到套接字实现均以 Windows Vista 及以上的版本为例进行分析,即 DualStackPlainSocketImpl。

2.2 主要属性

private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl;
private boolean oldImpl = false;
  • created 表示是否已经创建了 SocketImpl 对象,ServerSocket 需要依赖该对象实现套接字操作。
  • bound 是否已绑定地址和端口。
  • closed 是否已经关闭套接字。
  • closeLock 关闭套接字时用的锁。
  • impl 真正的套接字实现对象。
  • oldImpl 是不是使用旧的实现。

下面我们看一下 ServerSocket 的主要方法。

2.3 构造函数

有五类构造函数,可以什么参数都不传,也可以传入 SocketImpl、端口、backlog 和地址等。主要看一下最后一个构造函数,setImpl 方法用于设置实现对象,然后检查端口大小是否正确,检查 backlog 小于 0 就让它等于 50,最后进行端口和地址绑定操作。

public ServerSocket() throws IOException {
setImpl();
}

总结: 在 new ServerSocket() 时会通过 setImpl 方法创建一个 SocketImpl 的实现类,以 window 下 DualStackPlainSocketImpl 为例。时序图如下:

2.3.1 setImpl 方法

设置套接字实现对象,这里提供了工厂模式可以方便的对接其他的实现,而默认是没有工厂对象的,所以模式的实现为 SocksSocketImpl 对象。

private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}

总结: 从上面的方法可以看出构造方法只是创建了 SocksSocketImpl 对象,这些都只是 JDK 层面的东西,并没直接创建网络连接,bind 方法则会直接创建网络连接。

2.4 bind 方法

该方法用于将套接字绑定到指定的地址和端口上,如果 SocketAddress 为空,即代表地址和端口都不指定,此时系统会将套接字绑定到所有有效的本地地址,且动态生成一个端口。逻辑如下:

// 绑定端口,开启服务
public void bind(SocketAddress endpoint, int backlog) throws IOException {
// 省略...
if (endpoint == null)
endpoint = new InetSocketAddress(0);
if (backlog < 1)
backlog = 50;
getImpl().bind(epoint.getAddress(), epoint.getPort());
getImpl().listen(backlog);
}

总结: ServerSocket.bind 方法调用 impl.bind 和 impl.listen 创建网络连接,下面我们就重点分析这两个方法都做了些什么事。

注意: 地址是否为空,为空则创建一个 InetSocketAddress,默认是所有有效的本地地址,对应的为

0.0.0.0,而端口默认为0,由操作系统动态生成,backlog 如果小于 1 则设为 50。

2.4.1 socketCreate 方法

先看一下 createImpl 方法。

// ServerSocket
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}

总结: createImpl 将创建 socket 网络套接字的任务直接委任给了对应的 impl 实现类,在这里也就是 DualStackPlainSocketImpl。

// AbstractPlainSocketImpl
protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) { // UDP
ResourceManager.beforeUdpCreate();
// only create the fd after we know we will be able to create the socket
fd = new FileDescriptor();
try {
socketCreate(false);
} catch (IOException ioe) {
ResourceManager.afterUdpClose();
fd = null;
throw ioe;
}
} else { // TCP
fd = new FileDescriptor();
socketCreate(true);
}
if (socket != null)
socket.setCreated();
if (serverSocket != null)
serverSocket.setCreated();
}

总结: 在 AbstractPlainSocketImpl 抽象类中 create 方法对 UDP 和 TCP 协议分别做了处理,创建 socket 套接字的代码就一句 socketCreate(true),由具体的实现类完成。下面我们再看一下 DualStackPlainSocketImpl 是如何进行网络连接的。

// DualStackPlainSocketImpl
void socketCreate(boolean stream) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");
int newfd = socket0(stream, false /*v6 Only*/);
fdAccess.set(fd, newfd);
}

总结: socket0 是一个 native 方法,也就是上面 Linux 的 Socket 函数完成的。然后将返回的 Socket 句柄设置到 fd 对象中(FileDescriptor 是 JDK 对句柄的抽象)。

补充1:socket0 在 JVM 中的实现
// windows/native/java/net/DualStackPlainSocketImpl.c
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
(JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
int fd, rv, opt=0; // 最关键的一句代码,怎么样,是不是和 Linux 网络编程的 socket 函数一模一样的
fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
if (fd == INVALID_SOCKET) {
NET_ThrowNew(env, WSAGetLastError(), "create");
return -1;
} rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt));
if (rv == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "create");
} SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE);
return fd;
}

2.4.2 socketBind 方法

// AbstractPlainSocketImpl
protected synchronized void bind(InetAddress address, int lport) throws IOException {
synchronized (fdLock) {
if (!closePending && (socket == null || !socket.isBound())) {
NetHooks.beforeTcpBind(fd, address, lport);
}
}
socketBind(address, lport); // 绑定端口
if (socket != null)
socket.setBound();
if (serverSocket != null)
serverSocket.setBound();
}

总结: 和 create 方法一样 AbstractPlainSocketImpl 也把绑定端口 socketBind 交给子类 DualStackPlainSocketImpl 实现。

// DualStackPlainSocketImpl
void socketBind(InetAddress address, int port) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (address == null)
throw new NullPointerException("inet address argument is null."); bind0(nativefd, address, port, exclusiveBind);
if (port == 0) {
localport = localPort0(nativefd);
} else {
localport = port;
} this.address = address;
}

总结: bind0 也是一个 native 方法,下面看一下 Winidow 上的实现。

补充2:bind0 在 JVM 中的实现
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0
(JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port,
jboolean exclBind) {
SOCKETADDRESS sa;
int rv;
int sa_len = sizeof(sa); if (NET_InetAddressToSockaddr(env, iaObj, port, (struct sockaddr *)&sa,
&sa_len, JNI_TRUE) != 0) {
return;
}
// 最关键的一句代码,绑定端口
rv = NET_WinBind(fd, (struct sockaddr *)&sa, sa_len, exclBind); if (rv == SOCKET_ERROR)
NET_ThrowNew(env, WSAGetLastError(), "JVM_Bind");
}

2.4.3 socketListen 方法

// DualStackPlainSocketImpl
void socketListen(int backlog) throws IOException {
int nativefd = checkAndReturnNativeFD();
listen0(nativefd, backlog);
}
补充3:listen0 在 JVM 中的实现
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
(JNIEnv *env, jclass clazz, jint fd, jint backlog) {
// 关键的一句代码 listen 启动服务
if (listen(fd, backlog) == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "listen failed");
}
}

2.5 accept 方法

该方法用于接收套接字连接,套接字开启监听后会阻塞等待套接字连接,一旦有连接可接收了则通过该方法进行接收操作。

2.5.1 ServerSocket.accept

public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
Socket s = new Socket((SocketImpl) null);
implAccept(s);
return s;
}

总结: accept 做了三件事:一是判断套接字是否已经关闭。 二是判断套接字是否已经绑定。三是创建 Socket 对象,并调用 implAccept 接收连接。

2.5.2 ServerSocket.implAccept

protected final void implAccept(Socket s) throws IOException {
// 1. 创建一个空的 Socket 对象,用于接收 Socket 连接
SocketImpl si = null;
try {
if (s.impl == null)
s.setImpl();
else {
s.impl.reset();
}
si = s.impl;
s.impl = null;
si.address = new InetAddress();
si.fd = new FileDescriptor(); // 2. 最关键的一步,接收请求
getImpl().accept(si); SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccept(si.getInetAddress().getHostAddress(),
si.getPort());
}
} catch (IOException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
} catch (SecurityException e) {
if (si != null)
si.reset();
s.impl = si;
throw e;
}
// 3. 调用 Socket.postAccept 通知连接已经完成
s.impl = si;
s.postAccept();
}

总结: ServerSocket.implAccept 调用逻辑如下:

  1. 传入的 Socket 对象里面的套接字实现如果为空,则通过 setImpl 方法设置套接字实现,如果非空就执行 reset 操作。

  2. 调用套接字实现对象的 accept 方法完成接收操作,做这一步是因为我们的 Socket 对象里面的 SocketImpl 对象还差操作系统底层的套接字对应的文件描述符。

  3. 得到完整的 SocketImpl 对象,赋值给 Socket 对象,并且调用 postAccept 方法将 Socket 对象设置为已创建、已连接、已绑定。

2.5.3 AbstractPlainSocketImpl.accept

// AbstractPlainSocketImpl
protected void accept(SocketImpl s) throws IOException {
acquireFD(); // fdUseCount++
try {
socketAccept(s);// 接收请求
} finally {
releaseFD(); // fdUseCount--
}
}

总结: accept 直接委托给 socketAccept 方法。

2.5.4 DualStackPlainSocketImpl.socketAccept

void socketAccept(SocketImpl s) throws IOException {
// 1. 获取操作系统的文件描述符。
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null"); int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
// 2. timeout <= 0 则表示阻塞式接收连接
if (timeout <= 0) {
newfd = accept0(nativefd, isaa);
// 3. 如果 timeout 大于0,即设置了超时,那么会先非阻塞式接收连接
} else {
// 3.1 serverSocket 设置成非阻塞模式
configureBlocking(nativefd, false);
try {
// 3.2 本地方法,阻塞 timeout 时长用于获取新的 socket
waitForNewConnection(nativefd, timeout);
// 3.3 因为现在是非阻塞的,不管有没有连接都会马上返回
newfd = accept0(nativefd, isaa);
if (newfd != -1) {
// 3.4 新建立的 Socket 设置为阻塞模式
configureBlocking(newfd, true);
}
} finally {
// 3.5 serverSocket 恢复为阻塞模式
configureBlocking(nativefd, true);
}
} // 4. 将获取到的新文件描述符赋给 SocketImpl 对象,
// 同时也将远程端口、远程地址、本地端口等都赋给它相关变量。
fdAccess.set(s.fd, newfd);
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
}

总结: socketAccept 调用本地方法 accept0 接收连接。具体逻辑如下:

  1. 获取操作系统的文件描述符。
  2. SocketImpl 对象为空则抛出 NullPointerException("socket is null")。
  3. 如果 timeout 小于等于 0 则直接调用本地 accept0 方法,一直阻塞。
  4. 反之,如果 timeout 大于0,即设置了超时,那么会先调用 configureBlocking 本地方法,该方法用于将指定套接字设置为非阻塞模式。接着调用waitForNewConnection 本地方法,如果在超时时间内能获取到新的套接字,则调用 accept0 方法获取新套接字的句柄,获取成功后再次调用 configureBlocking 本地方法将新套接字设置为阻塞模式。最后,如果非阻塞模式失败了,则将原来的套接字设置会紫塞模式,这里使用了 finally,所以能保证就算发生异常也能被执行。
  5. 最后将获取到的新文件描述符赋给 SocketImpl 对象,同时也将远程端口、远程地址、本地端口等都赋给它相关变量。
补充4:configureBlocking 在 JVM 中的实现

本地方法逻辑很简单,如下,核心就是通过调用 Winsock 库的 ioctlsocket 函数来设置套接字为阻塞还是非阻塞,根据 blocking 标识。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_configureBlocking
(JNIEnv *env, jclass clazz, jint fd, jboolean blocking) {
u_long arg;
int result; if (blocking == JNI_TRUE) {
arg = SET_BLOCKING; // 0
} else {
arg = SET_NONBLOCKING; // 1
} result = ioctlsocket(fd, FIONBIO, &arg);
if (result == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "configureBlocking");
}
}
补充5:waitForNewConnection 在 JVM 中的实现

通过 Winsock 库的 select 函数来实现超时的功能,它会等待 timeout 时间看指定的文件描述符是否有活动,超时了的话则会返回 0,此时向 Java 层抛出 SocketTimeoutException 异常。而如果返回了 -1 则表示套接字已经关闭了,抛出 SocketException 异常。如果返回-2则抛出 InterruptedIOException。

JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForConnect
(JNIEnv *env, jclass clazz, jint fd, jint timeout) {
int rv, retry;
int optlen = sizeof(rv);
fd_set wr, ex;
struct timeval t; FD_ZERO(&wr);
FD_ZERO(&ex);
FD_SET(fd, &wr);
FD_SET(fd, &ex);
t.tv_sec = timeout / 1000;
t.tv_usec = (timeout % 1000) * 1000; rv = select(fd+1, 0, &wr, &ex, &t); if (rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"connect timed out");
shutdown( fd, SD_BOTH );
return;
} if (!FD_ISSET(fd, &ex)) {
return; /* connection established */
} for (retry=0; retry<3; retry++) {
NET_GetSockOpt(fd, SOL_SOCKET, SO_ERROR,
(char*)&rv, &optlen);
if (rv) {
break;
}
Sleep(0);
} if (rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
"Unable to establish connection");
} else {
NET_ThrowNew(env, rv, "connect");
}
}
补充6:accept0 在 JVM 中的实现
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_accept0
(JNIEnv *env, jclass clazz, jint fd, jobjectArray isaa) {
int newfd, port=0;
jobject isa;
jobject ia;
SOCKETADDRESS sa;
int len = sizeof(sa); memset((char *)&sa, 0, len);
newfd = accept(fd, (struct sockaddr *)&sa, &len); if (newfd == INVALID_SOCKET) {
if (WSAGetLastError() == -2) {
JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
"operation interrupted");
} else {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
"socket closed");
}
return -1;
} ia = NET_SockaddrToInetAddress(env, (struct sockaddr *)&sa, &port);
isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port);
(*env)->SetObjectArrayElement(env, isaa, 0, isa); return newfd;
}

总结: 本地方法 accept0 实现逻辑:

  1. 通过 C 语言的 memset 函数将 SOCKETADDRESS 联合体对应的结构体内的值设置为 0。
  2. 通过 Winsock 库的 accept 函数获取套接字地址。
  3. 判断接收的套接字描述符是否无效,分别可能抛 InterruptedIOException 或 SocketException 异常。
  4. 通过 SetHandleInformation 函数设置句柄的继承标志。
  5. NET_SockaddrToInetAddress 函数用于将得到的套接字转换成 Java 层的 InetAddress 对象。
  6. 将生成的 InetAddress 对象用于生成 Java 层的 InetSocketAddress 对象。
  7. 赋值给 Java 层的 InetSocketAddress 数组对象。
  8. 返回新接收的套接字的文件描述符。

2.6 总结

可以看到 ServerSocket 的核心方法都是 native 方法,是由 JVM 调用 linux 的内核函数完成的。想要对 Socket 网络编程有更详细的了解就必须进一步了解 Linux 网络编程

参考:

  1. JVM的ServerSocket是怎么实现的(上)

每天用心记录一点点。内容也许不重要,但习惯很重要!

NIO 源码分析(02-1) BIO 源码分析的更多相关文章

  1. JDK1.8源码分析02之阅读源码顺序

    序言:阅读JDK源码应该从何开始,有计划,有步骤的深入学习呢? 下面就分享一篇比较好的学习源码顺序的文章,给了我们再阅读源码时,一个指导性的标志,而不会迷失方向. 很多java开发的小伙伴都会阅读jd ...

  2. NIO 源码分析(03) 从 BIO 到 NIO

    目录 一.NIO 三大组件 Channels.Buffers.Selectors 1.1 Channel 和 Buffer 1.2 Selector 1.3 Linux IO 和 NIO 编程的区别 ...

  3. NIO 源码分析(02-2) BIO 源码分析 Socket

    目录 一.BIO 最简使用姿势 二.connect 方法 2.1 Socket.connect 方法 2.2 AbstractPlainSocketImpl.connect 方法 2.3 DualSt ...

  4. Spring源码解析02:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  5. 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 百篇博客分析OpenHarmony源码 | v52.02

    百篇博客系列篇.本篇为: v52.xx 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 51.c.h.o 前因后果相关篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 ...

  6. 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射 | 百篇博客分析OpenHarmony源码 | v42.02

    百篇博客系列篇.本篇为: v42.xx 鸿蒙内核源码分析(中断切换篇) | 系统因中断活力四射 | 51.c.h .o 硬件架构相关篇为: v22.xx 鸿蒙内核源码分析(汇编基础篇) | CPU在哪 ...

  7. 【图解源码】Zookeeper3.7源码分析,包含服务启动流程源码、网络通信源码、RequestProcessor处理请求源码

    Zookeeper3.7源码剖析 能力目标 能基于Maven导入最新版Zookeeper源码 能说出Zookeeper单机启动流程 理解Zookeeper默认通信中4个线程的作用 掌握Zookeepe ...

  8. Fresco 源码分析(一) DraweeView-DraweeHierarchy-DraweeController(MVC) DraweeView的分析

    4. Fresco的内容 为了方便学习,我们先从使用结合官方的文档来分析 4.1 Fresco客户端的使用 在使用Fresco的使用,我们直接使用的是SimpleDraweeView这个类,然后在Ac ...

  9. Zepto源码分析(一)核心代码分析

    本文只分析核心的部分代码,并且在这部分代码有删减,但是不影响代码的正常运行. 目录 * 用闭包封装Zepto * 开始处理细节 * 正式处理数据(获取选择器选择的DOM) * 正式处理数据(添加DOM ...

随机推荐

  1. PHP面试 PHP基础知识 十一(开发环境及相关配置)

    开发环境及相关配置 版本控制软件 集中式和分布式 集中式:集中式版本控制工具,版本库集中存放在中央服务器,团队成员里的每个人工作时从中央服务器下载代码,个人修改后再提交到中央服务器 分布式:分布式版本 ...

  2. Codeforces 578B "Or" Game (前缀和 + 贪心)

    Codeforces Round #320 (Div. 1) [Bayan Thanks-Round] 题目链接:B. "Or" Game You are given \(n\) ...

  3. upc组队赛5 Election of Evil【搜索】

    Election of Evil 题目描述 Dylan is a corrupt politician trying to steal an election. He has already used ...

  4. 20140903 dynamic_cast和static的区别 链表的冒泡排序和插入排序

    1.四个类型转换 http://www.cnblogs.com/chio/archive/2007/07/18/822389.html static_cast和dynamic_cast的区别 dyna ...

  5. 微信小程序 获取用户信息并保存登录状态

    微信小程序 获取用户信息并保存登录状态:http://www.360doc.com/content/18/0124/11/9200790_724662071.shtml

  6. 高级UI晋升之自定义view实战(七)

    更多Android高级架构进阶视频学习请点击:https://space.bilibili.com/474380680本篇文章自定义ViewGroup实现瀑布流效果来进行详解dispatchTouch ...

  7. python+tushare获取上市公司财务报表:资产负债表

    接口:balancesheet 描述:获取上市公司资产负债表 积分:用户需要至少500积分才可以调取,具体请参阅本文最下方积分获取办法 注:tushare包下载和初始化教程,请查阅我之前的文章 提示: ...

  8. vps被封逃逸_v2+cloudflare+websocket+tls+nginx

    每逢重大节日,总有那么一大部分vps凉凉,以下为能正经正常使用vps,无奈之举,此法由于多层代理,夜间速度会有影响,白天感受不明显. 由于博客园内容审查,v2_ray 中间的 下划线为分隔符,相关链接 ...

  9. 移动端(视口(meta),像素比,二倍图(图片,背景图,精灵图),css初始化(normalize.css),特殊样式,常见屏幕尺寸)

    1. 视口:(布局视口(layout viewport),视觉视口(visual viewport),理想视口(ideal viewport)) meta 视口标签 <meta  name = ...

  10. 【转载】vue-cli搭建的环境,用nginx做代理服务器,访问时显示:Invalid Host header

    来源:https://blog.csdn.net/Cookysurongbin/article/details/86077241 vue-cli搭建的环境,用nginx做代理服务器,访问时显示:Inv ...