JAVA网络编程-服务器Socket
ServerSocket
使用ServerSocket
处理服务端异常
阻塞
服务端队列
构造但不绑定端口
随机端口
Socket选项
服务器第一版
服务器第二版(重定向服务器)
ServerSocket
Java提供了一个ServerSocket类表示服务器Socket,举例来说,服务器Socket的任务就是坐在电话旁等电话.从技术上讲,服务器Socket在服务器主机上运行,监听入站TCP连接.每个服务器Socket监听服务器主机上的一个特定端口.当远程主机上的一个客户端尝试连接这个端口是,服务器Socket就被唤醒,协商建立客户端和服务器之间的连接,并返回一个常规的Socket对象,表示两台主机之间的Socket.换句话说,服务器Socket等待连接,而客户端Socket发起连接.一旦ServerSocket建立了连接,服务器会使用一个常规的Socket对象向客户端发送数据和接受客户端的数据.数据总是通过常规的Socket传输.
使用ServerSocket
在Java中,服务器程序的基本生命周期如下:
1 使用一个ServerSocket()构造函数在一个特定端口创建一个新的ServerSocket.
2 ServerSocket使用其accept()方法监听这个端口的入站连接.accept()会一直阻塞,直到一个客户端尝试建立连接,此时accept()将返回一个连接客户端和服务器的Socket对象.
3 调用Socket的getInputStream()和getOutputStream()方法,获得与客户端通信的输入和输出流.
4 服务器和客户端根据已协商的协议进行交互,直到要关闭连接.
5 服务器或客户端(或二者)关闭连接.
6 服务器返回到步骤2,等待下一次连接.
public static void main(String[] args)throws Exception { ServerSocket server = new ServerSocket(13); while (true) { Socket socket = server.accept(); OutputStream os = socket.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); bw.append("success"); bw.newLine(); bw.flush(); socket.close(); } }//服务端
public static void main(String[] args) throws Exception{ Socket socket = new Socket("127.0.0.1",13); InputStream in = socket.getInputStream(); BufferedReader bw = new BufferedReader(new InputStreamReader(in)); String s = null; while ((s=bw.readLine())!=null){ System.out.println("接收的数据"+s); } socket.close(); }//客户端
处理服务端异常
服务端异常处理时,代码会变得有些复杂.有两类异常,一类异常可能关闭服务器并记录一个错误信息,另一类异常只会关闭活动连接,区分这两类异常非常重要.
public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(13); while (true) { Socket socket = null; try { socket = server.accept(); OutputStream os = socket.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); bw.append("success"); bw.newLine(); bw.flush(); socket.close(); } catch (Exception e) { System.out.println("连接关闭"); } finally { if (socket != null) { try { socket.close(); } catch (Exception e) { } } } } } catch (Exception e) { System.out.println("服务器关闭了"); } finally { if (server != null) { try { server.close(); } catch (Exception e) { } } } }
阻塞
1 public static void main(String[] args) { 2 ServerSocket server = null; 3 try { 4 server = new ServerSocket(13); 5 while (true) { 6 Socket socket = null; 7 try { 8 socket = server.accept(); 9 OutputStream os = socket.getOutputStream(); 10 System.out.println("正在发送数据"); 11 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os)); 12 bw.append("success"); 13 bw.newLine(); 14 bw.flush(); 15 16 socket.shutdownOutput(); 17 18 System.out.println("正在接收数据"); 19 InputStream in = socket.getInputStream(); 20 BufferedReader br = new BufferedReader(new InputStreamReader(in)); 21 System.out.println(br.readLine()); 22 23 } catch (Exception e) { 24 System.out.println("连接关闭"); 25 } finally { 26 if (socket != null) { 27 try { 28 socket.close(); 29 } catch (Exception e) { 30 } 31 } 32 } 33 } 34 } catch (Exception e) { 35 System.out.println("服务器关闭了"); 36 } finally { 37 if (server != null) { 38 try { 39 server.close(); 40 } catch (Exception e) { 41 } 42 } 43 } 44 }//服务端
1 public static void main(String[] args) throws Exception{ 2 Socket socket = new Socket("127.0.0.1",13); 3 InputStream in = socket.getInputStream(); 4 System.out.println("正在接收数据"); 5 BufferedReader br = new BufferedReader(new InputStreamReader(in)); 6 String s = null; 7 while ((s=br.readLine())!=null){ 8 System.out.println("接收的数据"+s); 9 } 10 11 12 System.out.println("正在发送数据"); 13 OutputStream out = socket.getOutputStream(); 14 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out)); 15 bw.write("ddddddddd"); 16 bw.flush(); 17 18 socket.shutdownOutput(); 19 }//客户端
把服务端第16行注释掉,服务端和服务端的打印结果为
//这是服务端打印
正在发送数据
正在接收数据
//这是客户端打印
正在接收数据
接收的数据success
这种结果并不完全反映在打印上,其实此时客户端一直阻塞着,并未结束.原因是服务端写入数据后没有关闭输出流,客户端还在等待读取数据.这种情况会把客户端与服务端的连接一直阻塞.
把客户端第18行注释掉,服务端和客户端的打印结果为
//这是服务端打印 正在发送数据 正在接收数据 连接关闭
//这是客户端打印 正在接收数据 接收的数据success 正在发送数据
出现这种结果的原因是,服务端写完数据后关闭了写入流,客户端从而读取完毕.接着向服务端写入数据,但写入完毕后并没有关闭写入流,程序直接结束.服务端还在等待读数据,但是客户端已经结束,所以服务端抛出了异常.
服务端队列
管理客户连接请求的任务是由操作系统来完成的. 操作系统把这些连接请求存储在一个先进先出的队列中. 许多操作系统限定了队列的最大长度, 一般为 50 . 当队列中的连接请求达到了队列的最大容量时, 服务器进程所在的主机会拒绝新的连接请求. 只有当服务器进程通过 ServerSocket 的 accept() 方法从队列中取出连接请求, 使队列腾出空位时, 队列才能继续加入新的连接请求.
public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(15,1); while (true) { Socket socket = null; try { socket = server.accept(); Thread.sleep(1000*10); } catch (Exception e) { System.out.println("连接关闭"); } finally { if (socket != null) { try { socket.close(); } catch (Exception e) { } } } } } catch (Exception e) { System.out.println("服务器关闭了"); } finally { if (server != null) { try { server.close(); } catch (Exception e) { } } } }// 服务端
public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 15); }// 客户端
构造但不绑定端口
ServerSocket server = new ServerSocket(); SocketAddress address = new InetSocketAddress(15); server.bind(address);
随机端口
传入端口号为0,操作系统会为你选择可用端口,在FTP协议中这是很常见的,客户端首先连接到已知的21端口,不过在传输文件时,服务器开始监听任何可用的端口,然后服务器会使用已经在端口21打开的命令连接告诉客户端应当连接到哪一个端口来得到数据。
public static void main(String[] args) throws Exception{ ServerSocket server = new ServerSocket(0); System.out.println(server.getLocalPort()); while (true) { Socket socket = server.accept(); } }// 服务端
Socket选项
Socket选项制定了ServerSocket类所依赖的原生Socket如何发送和接收数据。对于服务器Socket,Java支持三个选项。
SO_TIMEOUT:SO_TIMEOUT是accept()在抛出java.io.InterruptedIOException异常前等待入站连接的时间,以毫秒为单位。如果SO_TIMEOUT为0,accept()就永远不会超时。这个默认值的作用就是永远不会超时。设置了该值并大于0则在指定的时间内没有客户端连接则抛出异常。
public static void main(String[] args) throws Exception{ ServerSocket server = new ServerSocket(15); server.setSoTimeout(1000*3); System.out.println(server.getSoTimeout()); while (true) { Socket socket = server.accept(); } }// 服务端
SO_REUSEADDR:服务端的SO_REUSEADDR与客户端的SO_REUSEADDR作用类似,它确定了是否允许一个新的Socket绑定到之前使用过的一个端口,而此时可能还有一些发送到原Socket的数据正在网络上传输。
public static void main(String[] args) throws Exception{ ServerSocket server = new ServerSocket(15); server.setReuseAddress(true); System.out.println(server.getReuseAddress()); while (true) { Socket socket = server.accept(); } }// 服务端
SO_RCVBUF:SO_RCVBUF服务器Socket接收客户端的缓冲区大小,设置一个服务器的SO_RCVBUF就像在accept()返回的各个Socket上调用setReceiveBufferSize()。如果你设置的缓冲区大于64KB则必须在绑定端口之前设置。
public static void main(String[] args) throws Exception{ ServerSocket server = new ServerSocket(); server.setReceiveBufferSize(1024*1024); System.out.println(server.getReceiveBufferSize()); server.bind(new InetSocketAddress(15)); while (true) { Socket socket = server.accept(); } }// 服务端
服务器第一版
以下代码片段为一个简单的HTTP服务器,无论接收到什么内容都返回一个固定的字符串,如果请求内容中包含HTTP则表示客户端可以解析HTTP相应,则返回固定的响应头+固定的字符串。注意,以下示例不能直接用浏览器访问!
package com.datang.bingxiang.demo; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.Charset; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SingleFileHTTPServer { private final byte[] content; private final byte[] header; private final int port; private final String encoding; public SingleFileHTTPServer(String data, String encoding, String mimeType, int port) throws Exception { this(data.getBytes(encoding), encoding, mimeType, port); } public SingleFileHTTPServer(byte[] data, String encoding, String mimeType, int port) { this.content = data; this.encoding = encoding; this.port = port; StringBuilder sb = new StringBuilder(); sb.append("HTTP/1.0 200 OK\r\n"); sb.append("Server: OneFile 2.0\r\n"); sb.append("Content-length: " + this.content.length + "\r\n"); sb.append("Content-type: " + mimeType + "; charset=" + encoding + "\r\n\r\n"); this.header = sb.toString().getBytes(Charset.forName("US-ASCII")); } public void start() throws Exception { ExecutorService pool = Executors.newFixedThreadPool(100); ServerSocket server = new ServerSocket(this.port); while (true) { Socket connection = server.accept(); pool.submit(new HTTPHandler(connection)); } } private class HTTPHandler implements Runnable { private final Socket connection; HTTPHandler(Socket connection) { this.connection = connection; } @Override public void run() { try { BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder request = new StringBuilder(80); String data = null; while ((data=in.readLine())!=null) { request.append(data); } connection.shutdownInput(); System.out.println("------"+request); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); //如果是HTTP/1.0或以后的版本,则发送一个MIME首部 if (request.toString().indexOf("HTTP/") != -1) { out.append(new String(header,encoding)); } out.append(new String(content,encoding)); out.flush(); connection.shutdownOutput(); } catch (Exception e) { e.printStackTrace(); }finally { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws Exception { SingleFileHTTPServer server = new SingleFileHTTPServer("我是服务器", "UTF-8", "text/plain", 80); server.start(); } }//服务端
package com.datang.bingxiang.demo; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.Socket; public class Client { public static void main(String[] args) throws Exception { Socket socket = new Socket("127.0.0.1", 80); OutputStream out = socket.getOutputStream(); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out)); bw.append("HTTP/1.1"); bw.flush(); socket.shutdownOutput(); InputStream in = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String line = null; while ((line = br.readLine()) != null) { System.out.println(line); } socket.shutdownInput(); socket.close(); }// 客户端 }
服务器第二版(重定向服务器)
以下DEMO支持GET请求和POST请求(请求体需要换行,看下图)在这里出现问题,不知道Tomcat服务器是如何处理的,无论是用字节流读取判断-1还是字符流读取判断null都无法正确的读取到末尾,所以只能根据不同的请求做不同的处理,如果是GET请求则在空行后直接停止循环,否则不会自己停止,如果是POST请求则需要拿到Content-Length请求头,然后从空行后开始计算Body的长度,注意你的Body一定要自动的加上空行,否则依然无法结束读取。就POST请求来说,我使用Tomcat服务器并不需要特意的指定结束行,这是一个严重的坑!
不过在浏览器访问该Demo是可以成功重定向的。
package com.datang.bingxiang.demo; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; public class Redirector { private final int port; private final String newSite; public Redirector(String newSite, int port) { this.port = port; this.newSite = newSite; } public void start() throws Exception { ServerSocket server = new ServerSocket(port); while (true) { Socket s = server.accept(); Thread t = new RedirectThread(s); t.start(); } } private class RedirectThread extends Thread { private final Socket connection; RedirectThread(Socket s) { this.connection = s; } public void run() { try { StringBuilder request = new StringBuilder(); InputStream inputStream = connection.getInputStream(); //字符流读取方式 BufferedReader in = new BufferedReader(new InputStreamReader(inputStream)); String methodLine = in.readLine(); String method = ""; if (methodLine.startsWith("GET")) { method = "GET"; } else if (methodLine.startsWith("POST")) { method = "POST"; } request.append(methodLine); int contentLength = -1; int bodyLnegth = 0; String line = null; boolean isBody = false; while ((line = in.readLine()) != null) { request.append(line + "\r\n"); System.out.println("!!!" + line+"--"+(line.equals(""))); if (line.startsWith("Content-Length")) { contentLength = Integer.parseInt(line.split(":")[1].trim()); } if (line.equals("")) { if (method.equals("GET")) { break; } else { isBody=true; System.out.println("读到空行了"); } } if(isBody) { bodyLnegth += line.length(); if(bodyLnegth==contentLength-2) { break; } } } System.out.println("----------------------------------------------"); //字节流读取方式 // byte[] b = new byte[1024]; // int len = 0; // while ((len = inputStream.read(b)) != -1) { // String s = new String(b, 0, len); // System.out.println(s); // request.append(s); // } connection.shutdownInput(); BufferedWriter out = new BufferedWriter( new OutputStreamWriter(connection.getOutputStream(), "US-ASCII")); // 如果是HTTP/1.0或以后版本,则发送一个Mime首部 if (request.toString().indexOf("HTTP") != -1) { out.write("HTTP/1.1 302 FOUND\r\n"); Date now = new Date(); out.write("Date: " + now + "\r\n"); out.write("Server: Redirector 1.1\r\n"); out.write("Location: " + newSite + "\r\n"); out.write("Content-type: text/html\r\n\r\n"); out.flush(); } // 并不是所有浏览器都支持重定向,所有我们需要生成HTML指出文档转移到哪里 out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n"); out.write("<BODY><H1>Document moved</H1>\r\n"); out.write("The document " + " has moved to\r\n<A href=\"" + newSite + "\">" + newSite + "</A>.\r\n Please update your bookmarks<P>"); out.write("</BODY></HTML>\r\n"); out.flush(); connection.shutdownOutput(); } catch (Exception e) { try { connection.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } public static void main(String[] args) throws Exception { int thePort = 1111; String theSite = "http://localhost:8888/test"; Redirector redirector = new Redirector(theSite, thePort); redirector.start(); } }// 服务端
@RequestMapping(value = "test") public String g(HttpServletRequest request) throws IOException { System.out.println(request.getRemotePort()); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = headerNames.nextElement(); Enumeration<String> headers = request.getHeaders(key); while (headers.hasMoreElements()) { String value = headers.nextElement(); System.out.println(key + " " + value); } } return "success1"; }// 服务端