如何用java.io.*写一个服务器

写一个服务器

最近在看java nio相关的东西,照着书上的代码写了一个示例,然后就遇到了问题。为什么我们的服务器,只能接受一个请求,就卡在那里了,他到底在干啥呢?

我们先写一个简单的server

package top.huster.server.bio;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

/***
 * @Author : rentianbing
 * @Date : 2021/3/11 10:26 上午
 *
 ***/
public class TimeServerV0 {
    private final static int SERVER_PORT = 9898;

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        final Socket socket = serverSocket.accept();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String content = bufferedReader.readLine();
        System.out.println("server : " + content);
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bufferedWriter.write("time is " + new Date());
        bufferedWriter.flush();
    }
}

然后写一个client去连接,

package top.huster.server.bio;

import java.io.*;
import java.net.Socket;

/***
 * @Author : rentianbing
 * @Date : 2021/3/11 10:33 上午
 *
 ***/
public class TimeServerClient {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("127.0.0.1", 9898);
        //构建IO
        InputStream is = s.getInputStream();
        OutputStream os = s.getOutputStream();

        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));
        //向服务器端发送一条消息
        bw.write("测试客户端和服务器通信,服务器接收到消息返回到客户端\n");
        bw.flush();

        //读取服务器返回的消息
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String mess = br.readLine();
        System.out.println("服务器:"+mess);
    }
}

先把server起起来,接着我们用client连上去,发现正常执行一次之后,server和client都结束了,这显然不符合我们的要求,我们要求的是server需要一直在,源源不断的接受用户请求,很简单,稍加改造,我们改成如下

package top.huster.server.bio;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

/***
 * @Author : rentianbing
 * @Date : 2021/3/11 10:26 上午
 *
 ***/
public class TimeServer {
    private final static int SERVER_PORT = 9898;

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        while (true) {
            final Socket socket = serverSocket.accept();
            new Thread(new Runnable() {
                public void run() {
                    try {
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        String content = bufferedReader.readLine();
                        System.out.println("server : " + content);
                        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                        bufferedWriter.write("time is " + new Date());

                        bufferedWriter.flush();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

    }
}

我们加了一个死循环进去,每当有连接进来的时候,我们交给另外一个线程去处理,但是就这么个简单的程序,发现client无法收到服务端的信息,除非断开服务端,这个是为什么呢 ?我们不得不去探究一下这个java.io包的一些类和他的功能了。

几个关键的类

ServerSocket

首先看看这个类,这个类是启动一个server所必须的类,这个类有4个构造方法,通过这个类的accept可以拿到一个和client端的Socket,而通过这个Socket就可以实现和client端的通信,读或者写都可以。记住这个类的核心关键,一个是构造方法,一个是accept

BufferedReader

接着就是读写数据,最重要的就是BufferedReader这个缓冲区,为什么要有这么个缓冲区呢? 因为网卡/磁盘的数据要到用户空间,是一个非常漫长的过程,他们的速率相差了至少10倍,因此需要开辟一个用户空间和硬件之间的一个缓冲区。
这个方法的构造函数里需要一个字节流,数据以字节流的形式进行传输。
他有个非常重要的函数 readLine,逐行读入

InputStreamReader

An InputStreamReader is a bridge from byte streams to character streams: It reads bytes and decodes them into characters using a specified
他是streams字节流和字节的一个桥梁,能够将byte解码成字符,传递给用户使用。

这些可能在最初接触的时候,觉得挺复杂的,暂时不用理解太多,知道怎么写,其实就是一个固定的模式。

为什么第二个版本的服务器没有数据输出

我们排查看到,我们的client端阻塞在了BufferedReader.readLine这个方法里,我们研究下这个方法。
里面最关键的有两段,其中一个bufferLoop是不断的从外设读取数据填充到缓冲区

 bufferLoop:
            for (;;) {

                if (nextChar >= nChars)
                    fill();
                if (nextChar >= nChars) { /* EOF */
                    if (s != null && s.length() > 0)
                        return s.toString();
                    else
                        return null;
                }
                boolean eol = false;
                char c = 0;
                int i;

                /* Skip a leftover '\n', if necessary */
                if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
                skipLF = false;
                omitLF = false;

第二段,不断的从缓冲区里拿数据,知道遇到了反斜杠r反斜杠n

 charLoop:
                for (i = nextChar; i < nChars; i++) {
                    c = cb[i];
                    if ((c == '\n') || (c == '\r')) {
                        eol = true;
                        break charLoop;
                    }
                }

                startChar = nextChar;
                nextChar = i;

                if (eol) {
                    String str;
                    if (s == null) {
                        str = new String(cb, startChar, i - startChar);
                    } else {
                        s.append(cb, startChar, i - startChar);
                        str = s.toString();
                    }
                    nextChar++;
                    if (c == '\r') {
                        skipLF = true;
                    }
                    return str;
                }

                if (s == null)
                    s = new StringBuffer(defaultExpectedLineLength);
                s.append(cb, startChar, i - startChar);
            }

关键的地方就在于fill()这个方法里,


        int n;
        do {
            n = in.read(cb, dst, cb.length - dst);
        } while (n == 0);

里面的in.read方法是阻塞的,当这个方法在遇到了
Reads characters into a portion of an array.  This method will block until some input is available, an I/O error occurs, or the end of the stream is reached.

所以我们的程序是在等着服务端给数据,知道遇到了IO错误或者服务端的断口,才会返回,也就才会终止charLoop的运行,这样我们就知道了,为什么我们的客户端被卡住了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注