[ServerSocket概述]
server socket等待连接. 与之对应的是, client socket发起连接.
java中基本的ServerSocket生命周期如下:
import java.net.*;
import java.util.*;
import java.io.*;
public class DateTimeClient
{
public static final int PORT = 13;
public static void main (String[] args) throws UnknownHostException, IOException
{
try (Socket socket = new Socket("127.0.0.1",PORT)) {
Reader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
char[] arr = new char[30];
reader.read(arr);
System.out.println(arr);
reader.close();
socket.close();
}
catch (ConnectException e){
System.out.println("拒绝访问");
}
}
}
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
public class DayTimeServer
{
public static final int PORT = 13;
public static void main(String[] args)
{
try
{
// 创建一个监听13端口的ServerSocket
ServerSocket server = new ServerSocket(PORT);
while (true)
{
try
{
// accept()是个阻塞方法
Socket conn = server.accept();
Writer out = new BufferedWriter
(new OutputStreamWriter
(conn.getOutputStream()));
Date now = new Date();
out.write(now.toString()+"\r\n");
out.flush();
conn.close();
}
catch(IOException e)
{
/*
* 如果客户端在服务器仍在处理请求的时候关闭连接
* socket的io流就会在下一个读写操作
* 抛出InterrputedIOException
* 这种情况不应该导致服务器停摆.
*/
}
}
}
catch (IOException e)
{
System.err.println(e);
}
}
}
这里需要注意的是: accept()方法
1. accept方法的语义是: Listens for a connection to be made to this socket and accepts it.The method blocks until a connection is made. 监听为这个ServerSocket所做的连接,并且接受这个连接. 这个方法是阻塞态的,除非创立一个连接.
2. accept方法是阻塞的, 也就是说: 调用accept的方法会一直等待accept接受一个连接并以socket形式返回这个连接.
那么问题来了: accept会把main方法阻塞住. 如果客户端因为网络延迟等原因, 花了很久才连接上服务器, 就耽误了其他客户端的时间, 带来很差的用户体验.为了避免这样的问题, 我们引入多线程和异步IO两种方案. 本文重点讨论多线程解决方案.
----------------------- 一. 一个连接一个线程(thread per connection)方案---------------
原理如图所示:
import java.net.*;
import java.io.*;
import java.util.*;
public class MultithreadedDaytimeServer
{
public final static int PORT = 13;
public static void main(String[] args)
{
try (ServerSocket server = new ServerSocket(13))
{
while (true)
{
try //不能写成try (Socket connection = server.accept())
{
Socket connection = server.accept();
Thread task = new DaytimeThread(connection);
task.start();
}
catch(IOException e)
{
}
}
}
catch (IOException e)
{
System.out.println("服务器挂了!!好好检查代码吧!!");
}
}
}
class DaytimeThread extends Thread
{
private Socket connection;
DaytimeThread(Socket connection)
{
this.connection = connection;
}
@SuppressWarnings("deprecation")
@Override
public void run()
{
try {
Writer out = new BufferedWriter(
new OutputStreamWriter(
connection.getOutputStream()));
Date now = new Date();
out.write(now.toString()+"\r\n");
out.flush();
}
catch (IOException e)
{
System.err.println(e);
}
finally
{
try {
connection.close();
}
catch (IOException e) {
}
}
}
}
注意到内层的try/catch故意没有用小括号语法(英文名叫try-with-resources, 不知道中文叫啥, 区区不才给它起名叫”有源try”)
这个有源try是在java 7引进的语法糖, 要求小括号里面只能用来声明/定义AutoClosable对象, 执行完try/catch块之后, 自动关闭该对象. 鉴于以往版本的java在try/catch之后,还要在finally里面手动关闭socket,stream,channel之类的东西, 显得很罗嗦, 所以引入了这个语法糖.
之所以内层的try/catch不能使用语法糖, 是因为通讯线程和main线程都持有socket对象的引用, 当通讯线程开始运行的时候, main线程也不耽误, 照常执行thread之后的语句. main在执行完try/catch的时候socket已经被自动关闭. 此时在通讯线程里使用的就是已经关闭的socket了.
这个解决方案本身有一个大问题: 假如有恶意攻击者频繁发送请求, 那么JVM就会开很多很多线程,这会导致JVM内存不足从而崩溃. 要想限制通讯线程的数量, 就要使用线程池!
--------------------------------- 二. 使用线程池的方案---------------------------------------
import java.io.*;
import java.util.*;
import java.net.*;
import java.util.concurrent.*;
public class PooledDatetimeServer
{
public static final int PORT = 13;
public static void main (String[] args)
{
// java.util.concurrent.ExecutorService
ExecutorService pool = Executors.newFixedThreadPool(50);
try (ServerSocket server = new ServerSocket(13))
{
while (true)
{
try
{
Socket conn = server.accept();
Callable<Void> task = new DatetimeTask(conn);
pool.submit(task); //加入线程池
System.out.println("submitted");
}
catch (IOException e)
{
System.err.println("A client discarded connection");
}
}
}
catch (IOException e)
{
System.err.println("The server is crashed and need to restart");
}
}
}
class DatetimeTask implements Callable<Void>
{
private Socket conn;
public DatetimeTask(Socket connection)
{
this.conn = connection;
}
@Override
public Void call() throws Exception {
// TODO Auto-generated method stub
try
{
Writer out = new BufferedWriter
(new OutputStreamWriter(
conn.getOutputStream()));
Date now = new Date();
out.write(now.toString()+"\r\n");
out.flush();
}
catch (IOException e)
{
System.err.println(e);
}
finally
{
try
{
conn.close();
}
catch(IOException e){
e.printStackTrace();
}
}
return null;
}
}