Java Socket Server的演进 (一)

最近在看一些网络服务器的设计, 本文就从起源的角度介绍一下现代网络服务器处理并发连接的思路, 例子就用java提供的API。

1.单线程同步阻塞式服务器及操作系统API

此种是最简单的socket服务器了,完全不考虑多连接的问题,主线程一次只处理一个连接,其他的连接由操作系统保持,用的是java socket包的ServerSocket,其构造函数支持的backlog就是TCP连接的等待队列

* The maximum queue length for incoming connection indications (a
* 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>&quot;start read.&quot;<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(&quot;HTTP-Version Status-Code Reason-Phrase CRLF
HTTP/1.1 200 OK
&quot;<span style="color: #000000">).getBytes());
            Thread.sleep(</span>1000<span style="color: #000000">);
            log.info(</span>&quot;awake.&quot;<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:

image_thumb6

服务器的console上响应,显示读到的数据:

image_thumb8

Telnet2 在telnet1之后连上测试服务器IP 1.132,并打入tt4:

image_thumb15

服务器没有显示输出,但其实socket已经连上,服务器线程被阻塞在还没有处理完成的telnet1上,没有返回,所以虽然操作系统已经接受了telnet2的连接,但是应用程序无法处理。windows下可以netstat观察一下连接情况,以下图显示两个1.121的连接已经建立,处于ESTABLISHED状态。

image_thumb4

这时停掉telnet1, 既让socket断开,如下:

image_thumb11

这时可以看到telnet2之前输入的tt4在服务器端才有响应,说明之前telnet2的输入被缓冲在操作系统的缓冲区。

image_thumb13

实际的处理流程就是这个样子,incoming连接被排成队列一个一个由 while(true)中的socketServer.accept方法一个一个处理:

image_thumb21

这种只能处理一个连接的服务器程序明显是很鸡肋的,没有服务器会这样处理。那么为什么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类。

image_thumb

进来发现已经是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版的,有点费解, 
image_thumb2
该方法源码为:
/*
 * 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">&quot;</span><span style="color: #800000">SocketException</span><span style="color: #800000">&quot;</span><span style="color: #000000">,
                    </span><span style="color: #800000">&quot;</span><span style="color: #800000">Socket closed</span><span style="color: #800000">&quot;</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)-&gt;<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">&quot;</span><span style="color: #800000">SocketException</span><span style="color: #800000">&quot;</span><span style="color: #000000">,
                   </span><span style="color: #800000">&quot;</span><span style="color: #800000">Listen failed</span><span style="color: #800000">&quot;</span><span style="color: #000000">);
}

}

可以看到后面调用了JVM_Listen这个方法,继续搜索这个方法在哪:

image_thumb5

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

image_thumb9

backlog属性还是win32 API的底层设施支持的。

本文出自 “祝坤荣” 博客,请务必保留此出处

本文出自 “祝坤荣” 博客,请务必保留此出处
原文地址:https://www.cnblogs.com/zhukunrong/p/3588570.html