写一个服务器
最近在看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的运行,这样我们就知道了,为什么我们的客户端被卡住了。