最近在看一些网络服务器的设计, 本文就从起源的角度介绍一下现代网络服务器处理并发连接的思路, 例子就用java提供的API。
1.单线程同步阻塞式服务器及操作系统API
此种是最简单的socket服务器了,完全不考虑多连接的问题,主线程一次只处理一个连接,其他的连接由操作系统保持,用的是java socket包的ServerSocket,其构造函数支持的backlog就是TCP连接的等待队列
* request to connect) is set to the <code>backlog</code> parameter. If
* a connection indication arrives when the queue is full, the
* connection is refused.
public ServerSocket(int port, int backlog) throws IOException { this(port, backlog, null); }
server的样例代码,纯测试用途,不考虑优雅问题了:
public class SocketServer{ Logger log = getLogger("SocketServer"); ServerSocket server = null; public SocketServer() throws IOException { server = new ServerSocket(8080,50); System.out.println("Server start... listen on:8080" + server.getInetAddress().toString()); } public void service() throws InterruptedException { while(true) { try { log.info("wait connection..."); Socket socket = server.accept(); log.info(socket.toString()); InputStream is = socket.getInputStream(); Scanner scan = new Scanner(is); byte[] buffer = new byte[1024];</span><span style="color: #0000ff">while</span><span style="color: #000000"> (scan.hasNextLine()){ System.out.println(</span>"start read."<span style="color: #000000">); String str </span>=<span style="color: #000000"> scan.nextLine(); System.out.println(str); } socket.getOutputStream().write(</span><span style="color: #0000ff">new</span> String("HTTP-Version Status-Code Reason-Phrase CRLF HTTP/1.1 200 OK "<span style="color: #000000">).getBytes()); Thread.sleep(</span>1000<span style="color: #000000">); log.info(</span>"awake."<span style="color: #000000">); socket.close(); } </span><span style="color: #0000ff">catch</span><span style="color: #000000"> (IOException e) { log.severe(e.getMessage()); </span><span style="color: #008000">//</span><span style="color: #008000">To change body of catch statement use File | Settings | File Templates.</span>
}
}
}</span><span style="color: #0000ff">public</span> <span style="color: #0000ff">static</span> <span style="color: #0000ff">void</span><span style="color: #000000"> main(String[] args){ </span><span style="color: #0000ff">try</span><span style="color: #000000">{ SocketServer ss </span>= <span style="color: #0000ff">new</span><span style="color: #000000"> SocketServer(); ss.service(); } </span><span style="color: #0000ff">catch</span><span style="color: #000000"> (Exception e){ e.printStackTrace(); } }</span></pre>
由于socketserver的backlog,既等待队列设为50,所以下面的测试通过telnet客户端连接看阻塞式服务器对连接的处理。
Telnet1 先连上测试服务器IP 1.132,并打入tt3:
服务器的console上响应,显示读到的数据:
Telnet2 在telnet1之后连上测试服务器IP 1.132,并打入tt4:
服务器没有显示输出,但其实socket已经连上,服务器线程被阻塞在还没有处理完成的telnet1上,没有返回,所以虽然操作系统已经接受了telnet2的连接,但是应用程序无法处理。windows下可以netstat观察一下连接情况,以下图显示两个1.121的连接已经建立,处于ESTABLISHED状态。
这时停掉telnet1, 既让socket断开,如下:
这时可以看到telnet2之前输入的tt4在服务器端才有响应,说明之前telnet2的输入被缓冲在操作系统的缓冲区。
实际的处理流程就是这个样子,incoming连接被排成队列一个一个由 while(true)中的socketServer.accept方法一个一个处理:
这种只能处理一个连接的服务器程序明显是很鸡肋的,没有服务器会这样处理。那么为什么ServerSocket的构造函数支持这个backlog的缓冲队列呢? 下面在看看JVM代码中封装了什么
Backlog属性的本地代码
前面提到的backlog属性,JVM如何将这个队列与本地操作系统API结合呢,先看下构造ServerSocket的部分代码:
public void bind(SocketAddress endpoint, int backlog) throws IOException { ... if (backlog < 1) backlog = 50; try { SecurityManager security = System.getSecurityManager(); if (security != null) security.checkListen(epoint.getPort()); getImpl().bind(epoint.getAddress(), epoint.getPort()); getImpl().listen(backlog); bound = true; }...
}
可以看到调用了SocketImpl的listen方法,这个listen方法又做了什么呢?先看一下,SocketImpl类有几个实现类,这里直接根据名字直接选了PlainSocketImpl类。
进来发现已经是native代码了:
private native void socketListen(int count) throws IOException;
想看JVM干了什么,只能找OpenJDK的source代码了。(代码在linux下获取比较简单,
hg clone http://hg.openjdk.java.net/jdk7/jdk7 jdk7_tl;
运行./get_source.sh,不细说了, 不过找这个native方法的时候遇到点问题,发现没有windows版的同名c代码,只有solaris版的,有点费解,
该方法源码为:
/* * Class: java_net_PlainSocketImpl * Method: socketListen * Signature: (I)V */ JNIEXPORT void JNICALL Java_java_net_PlainSocketImpl_socketListen (JNIEnv *env, jobject this, jint count) { /* this FileDescriptor fd field */ jobject fdObj = (*env)->GetObjectField(env, this, psi_fdID); /* fdObj's int fd field */ int fd;</span><span style="color: #0000ff">if</span><span style="color: #000000"> (IS_NULL(fdObj)) { JNU_ThrowByName(env, JNU_JAVANETPKG </span><span style="color: #800000">"</span><span style="color: #800000">SocketException</span><span style="color: #800000">"</span><span style="color: #000000">, </span><span style="color: #800000">"</span><span style="color: #800000">Socket closed</span><span style="color: #800000">"</span><span style="color: #000000">); </span><span style="color: #0000ff">return</span><span style="color: #000000">; } </span><span style="color: #0000ff">else</span><span style="color: #000000"> { fd </span>= (*env)-><span style="color: #000000">GetIntField(env, fdObj, IO_fd_fdID); } </span><span style="color: #008000">/*</span><span style="color: #008000"> * Workaround for bugid 4101691 in Solaris 2.6. See 4106600. * If listen backlog is Integer.MAX_VALUE then subtract 1. </span><span style="color: #008000">*/</span> <span style="color: #0000ff">if</span> (count == <span style="color: #800080">0x7fffffff</span><span style="color: #000000">) count </span>-= <span style="color: #800080">1</span><span style="color: #000000">; </span><span style="color: #0000ff">if</span> (JVM_Listen(fd, count) ==<span style="color: #000000"> JVM_IO_ERR) { NET_ThrowByNameWithLastError(env, JNU_JAVANETPKG </span><span style="color: #800000">"</span><span style="color: #800000">SocketException</span><span style="color: #800000">"</span><span style="color: #000000">, </span><span style="color: #800000">"</span><span style="color: #800000">Listen failed</span><span style="color: #800000">"</span><span style="color: #000000">); }
}
可以看到后面调用了JVM_Listen这个方法,继续搜索这个方法在哪:
OK,这个路径下hotspot/src/share/vm/prims/jvm.cpp 的包装方法看起来很像,找过来看一下内容:
JVM_LEAF(jint, JVM_Listen(jint fd, jint count)) JVMWrapper2("JVM_Listen (0x%x)", fd); //%note jvm_r6 return os::listen(fd, count); JVM_END
发现是调用os::listen的,再到这个文件的头上找os的引用:
#include "runtime/os.hpp"
因为我是windows系统,绕了一圈,还是直接msdn上找到winsock的listen方法,
http://msdn.microsoft.com/en-us/library/windows/desktop/ms739168(v=vs.85).aspx
backlog属性还是win32 API的底层设施支持的。
本文出自 “祝坤荣” 博客,请务必保留此出处