前几天看到室友在码代码,我凑过去看了一下,原来在做socket编程啊!刚好这块内容我不熟悉,于是趴在他的桌上问了一些关于socket的问题。
简单来说他想要实现自己的服务端,然后对发送过来的请求做出响应。
诶,这个和我们熟悉的Tomcat服务器很像嘛。不错,Tomcat就是一个免费的开放源代码的Servlet容器。这篇我来实现一个简单的servlet容器的功能。
先来看一下Tomcat的处理流程,如下:
1、Web客户向Servlet容器(Tomcat)发出Http请求
2、Servlet容器分析客户的请求信息
3、Servlet容器创建一个HttpRequest对象,将客户请求的信息封装到这个对象中
4、Servlet容器创建一个HttpResponse对象
5、Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数
传给 HttpServlet对象
6、HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息
7、HttpServlet调用HttpResponse对象的有关方法,生成响应数据
8、Servlet容器把HttpServlet的响应结果传给Web客户
这里主要是实现一个简单网络交互的过程,所以在代码里我开启两个线程作为服务端和客户端来进行信息的发送和接受。
main线程:
public static void main(String[] args){
try {
Thread threadClient = new Thread(new ClientRunnable());
Thread threadServer = new Thread(new ServerRunnable());
threadServer.start();
/**让服务器充分运行**/
Thread.sleep(2000);
System.out.println("服务端正在等待接受信息....");
Thread.sleep(2000);
threadClient.start();
}catch (Exception e){
e.printStackTrace();
}
}
再来看服务端线程:
static class ServerRunnable implements Runnable{
@Override
public void run() {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
System.out.println("服务器启动成功 ....");
ServerSocket serverSocket = new ServerSocket(8081, 1, InetAddress.getByName("127.0.0.1"));
/*isRun标识是用来关闭该线程*/
while(isRun){
/*accept是线程阻塞的,即如果没有接受到socket,程序会一直被阻塞在这。*/
Socket socket = serverSocket.accept();
/*服务器接受客户端传来的信息*/
inputStream = socket.getInputStream();
StringBuilder sb = new StringBuilder();
byte[] bytes = new byte[1024];
int len = 0;
while((len=inputStream.read(bytes))!=-1){
sb.append(new String(bytes,0,len));
}
System.out.println("服务器接收到一条请求 ....");
System.out.println("服务器接受到的信息为: "+sb);
/*服务器对客户端的信息作出回应*/
outputStream = socket.getOutputStream();
outputStream.write("html文件来了".getBytes());
/*为什么要使用shutdownOutput,在后面的博客中会给出现详细说明*/
socket.shutdownOutput();
Thread.sleep(1000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try{
inputStream.close();
outputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
客户端线程:
static class ClientRunnable implements Runnable{
@Override
public void run() {
OutputStream outputStream = null;
InputStream inputStreams = null;
try {
System.out.println("客户端启动成功 ....");
Socket socket = new Socket("127.0.0.1", 8081);
/*客户端发送请求信息*/
outputStream = socket.getOutputStream();
Thread.sleep(1000);
System.out.println("客户端发送一个请求");
outputStream.write("服务器我想要一个html文件".getBytes());
socket.shutdownOutput();
/*客户端接受服务器的响应*/
inputStreams = socket.getInputStream();
byte[] bytes = new byte[1024];
StringBuilder stringBuilder = new StringBuilder();
int len = 0;
while((len = inputStreams.read(bytes))!=-1){
stringBuilder.append(new String(bytes,0,len));
}
System.out.println("客户端收到响应:"+stringBuilder);
/*接受完响应信息后关闭服务端的程序*/
isRun = false;
}catch (Exception e){
e.printStackTrace();
}finally {
try{
inputStreams.close();
outputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
volatile static boolean isRun = true;//用volatile修饰,保证它的可见性。
不明白volatile原理的点击这里
看一下运行的结果:
看到这,有人就会有疑惑。诶,这不是要讲NIO吗,你一直在讲socket干嘛。如果大家对IO了解的话,是知道read是线程阻塞的,即如果read没有读到数据是会一直等待的。就是说如果对于每一个socket请求,我们在服务端开一个线程去处理,但是该线程由于IO阻塞,不能及时的退出。
这种方式具有很高的响应速度,并且控制起来也很简单,在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程的无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况。 1000个socket我就需要开1000个线程去处理,这样做是会造成资源使利用率低下。
非阻塞式IO的出现的目的就是为了解决这个瓶颈。而非阻塞式IO是怎么实现的呢?
非阻塞IO在工作的时候,如果socket中数据并未准备好(即内容并没有从网络上接受完毕),会直接返回读取失败,而不是一直处于等到中。可是没有数据那怎么办,有人就想要么写在while(true){}就好了。但在实际中,这样编写也是不合理的。
以下selector这段引用与 零度博客 http://blog.youkuaiyun.com/zmx729618/article/details/51860699
Selector. 像一个巡警,在一个片区里面不停的巡逻. 一旦发现事件发生,立刻将事件select出来.不过这些事件必须是提前注册在selector上的. select出来的事件打包成SelectionKey.里面包含了事件的发生事件,地点,人物. 如果警察不巡逻,每个街道(socket)分配一个警察(thread),那么一个片区有几条街道,就需要几个警察.但现在警察巡逻了,一个巡警(selector)可以管理所有的片区里面的街道(socketchannel).
以上把警察比作线程,街道比作socket或socketchannel,街道上发生的一切比作stream.把巡警比作selector,引起巡警注意的事件比作selectionKey.
从上可以看出,使用NIO可以使用一个线程,就能维护多个持久TCP连接.
这也就是NIO的优势,一定程度上减少服务器瞬间的并发线程数,从而提高CPU执行效率。