Socket与NIO形式的区别
Socket是最基础的网络编程技术,HttpURLConnection和HttpClient都是基于Socket的。下面来看一个从服务器端下载文件到客户端的例子。
服务器端:
package org.huodong.action; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class FilmServer { public static void main(String[] args) { FilmServer ms = new FilmServer(); try { ms.server(); } catch (Exception e) { e.printStackTrace(); } } /** * 服务器端响应请求 标签: * * @throws Exception */ public void server() throws Exception { // 0.建立服务器端的server的socket ServerSocket ss = new ServerSocket(8089); while (true) { // 1.打开socket连接 // 等待客户端的请求 final Socket server = ss.accept(); //阻塞,一直等待直到有客户端的请求过来,如果客户端是两个线程的话,那么一定是一个线程的客户端的连接关闭了才能接到另一个线程的客户端的请求。 System.out.println("服务-----------请求开始start"); // 2.打开socket的流信息,准备下面的操作 final InputStream is = server.getInputStream(); byte b[] = new byte[1024]; int readCount = is.read(b); String str = new String(b); str = str.trim(); final String serverFileName = str; // 3.对流信息进行读写操作 System.out.println("客户端传过来的信息是:" + str); System.out.println("线程" + Thread.currentThread().getName() + "启动"); try { FileInputStream fileInputStream = new FileInputStream( serverFileName); OutputStream os = server.getOutputStream(); // 往客户端写文件 byte[] bfile = new byte[1024]; while (fileInputStream.read(bfile) > 0) { os.write(bfile); } fileInputStream.close(); os.close(); // 4.关闭socket // 先关闭输入流 is.close(); // 最后关闭socket server.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("服务-----------请求结束over"); } } }
客户端:
package org.huodong.action; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; import java.util.Date; public class FilmClient{ public static void main(String[] args) { for (int i = 1; i <= 2; i++) { Client client = new Client(); client.i = i; client.start(); } } } class Client extends Thread { int i; @Override public void run() { Date date = new Date(); // 1.建立scoket连接 Socket client; try { client = new Socket("127.0.0.1", 8089); // 2.打开socket的流信息,准备下面的操作 OutputStream os = client.getOutputStream(); // 3.写信息 os.write(("d://film//音乐.rar").getBytes()); //这个是服务器端的文件地址 String filmName = "e://io"+i+".rar"; //这是要下载到客户端的地址及文件名,这里相当于下载了两遍2.rmvb,只不过保存在客户端的时候起了两个不同的文件名,方便比较 FileOutputStream fileOutputStream = new FileOutputStream(filmName); System.out.println("Time="+date.getTime()); InputStream is = client.getInputStream();// 接收服务器端的文件并写到客户端,这里会一直等服务器端发消息过来,如果服务器sleep10秒才发送过来,客户端也会一直等, // 这就导致整个线程都会阻塞在这里 byte b[] = new byte[1024]; while(is.read(b)>0){ fileOutputStream.write(b); } // 4.关闭socket // 先关闭输出流 os.close(); // 最后关闭socket client.close(); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("Time1="+date.getTime()); } }
客户端启动了2个线程进行下载电影的工作,先启动服务端,再运行客户端,会看笔者本地的硬盘C分区到有如下效果。
可以看到线程2的下载任务一直是0字节,等第一个线程下载完成后呢,线程2的下载任务才能进行。
服务端的代码造成的问题就是使用传统的sokect网络通讯,那么另一个客户端的线程请求到server端的时候就发生了阻塞的情况,也就是说,服务端相当一个厕所,厕所就有只有一个坑位,来了一个人,相当于客户端请求,那这个人相当于就把坑位给占了,write操作和read操作会阻塞,这个人还没解决完问题呢,下个人就来了,没办法,哥们儿先在门外等等啊,等前一个客户爽完了再给您提供服务好吧。那么如何解决这个占着坑位不让别人用的情况呢?
3. 阻塞的多线程
为了解决以上问题,那么之后很多Server肯定不可能像以上程序那么做,不过以前很多Server都是基于单线程服务改造一下,做成多线程的Server的通讯,修改一下上面的Server代码,如下
package org.huodong.action; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * 以上的Server就是在原始的socket基础上加了线程,每一个Client请求过来后,整个Server主线程不必处于阻塞状态, * 接收请求后直接另起一个新的线程来处理和客户端的交互,就是往客户端发送二进制包。这个在新线程中虽然阻塞, * 但是对于服务主线程没有阻塞的影响,主线程依然通过死循环监听着客户端的一举一动。 * 另一个客户端的线程发起请求后就再起一个新的线程对象去为客户端服务。 */ public class FilmServerNewThread { public static void main(String[] args) { FilmServerNewThread ms = new FilmServerNewThread(); try { ms.server(); } catch (Exception e) { e.printStackTrace(); } } /** * 服务器端响应请求 * * @throws Exception */ public void server() throws Exception { // 0.建立服务器端的server的socket ServerSocket ss = new ServerSocket(8089); while (true) { // 1.打开socket连接 // 等待客户端的请求 final Socket server = ss.accept(); System.out.println("服务-----------请求开始start"); // 2.打开socket的流信息,准备下面的操作 final InputStream is = server.getInputStream(); byte b[] = new byte[1024]; int readCount = is.read(b); String str = new String(b); str = str.trim(); final String serverFileName = str; // 3.对流信息进行读写操作 System.out.println("客户端传过来的信息是:" + str); if (readCount > 0) { new Thread() { @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "启动"); try { FileInputStream fileInputStream = new FileInputStream( serverFileName); // 3.1 服务器回复客户端信息(response) OutputStream os = server.getOutputStream(); byte[] bfile = new byte[1024]; // 往客户端写 while (fileInputStream.read(bfile) > 0) { os.write(bfile); } fileInputStream.close(); os.close(); // 4.关闭socket // 先关闭输入流 is.close(); // 最后关闭socket server.close(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }.start(); } System.out.println("服务-----------请求结束over"); } } }
执行效果如下
2个线程互不影响,各自下载各自的。当然从非常严格的意义来讲,str变量在十分高并发的情况下有线程安全问题,这个咱暂且忽略,就着眼于低并发的情况。这个问题是什么呢,就是如果客户端请求比较多了,那么为每一个客户端开辟一个新的线程对象来处理网络传输的请求,需要创建个线程对象,而且这个线程对象从时间上来讲还是处于长连接,这个就比较消费系统资源,这个打开进程管理器就可以看到。而且每一个线程内部都是阻塞的,也没有说完全利用好这个新创建的线程。还拿刚才上厕所举例子,好比现在不止一个坑位了,来了一个用户我这边就按照工程师的厕所坑位图建立一个新的坑位,客户来了,不用等待老坑位,用新创建的坑位就行了。等那个老坑位用完了,自然有垃圾回收器去消灭那个一次性的坑位的,腾出资源位置为了建立新的坑位。长时间连接的意思,相当于这个人上厕所的时间非常长,便秘??需要拉一天才能爽完……老的坑位一时半会儿回收不了,新的坑位需要有空间为其建造茅房以便满足客户端的“急切方便”需要。久而久之,线程数目一多,系统就挂了的概率就增多了(谁也别想上,全玩完了)。
NIO方式与BIO(Socket)的方式的最大区别就是NIO是一请求一线程,而BIO是一连接一线程,后者就导致如果服务器未来得及响应客户端就要一直等一直等,该连接就一直占用着这个线程。如果是NIO的话则可以复用连接,也就是无须等待服务器端的响应就可以继续给服务器端发送消息,发送和接收是异步响应的。