简介
基本的TCP相应服务器是一次只能处理一个客户端请求,无法处理同时多个客户端请求,Java中多线程技术解决这一问题。多线程有两种方式:一是一客户一线程;二是线程池;
1)一客户一线程:即为每个连接创建一个线程来处理,服务器端会循环执行,监听指定端口的连接,反复接收来自客户端的连接请求,并为每个连接创建一个新线程来对其处理。
缺点:一客户一线程的方式虽然处理可以多个客户端请求,但每个新线程都会消耗系统资源(如CPU),并且每个线程独有自己的数据结构(如栈)也要消耗系统内存。另外一个线程阻塞是,JVM会保存其状态,选择另外一个线程运行,并在上下文转换时恢复阻塞线程的状态。随着线程数的增加,线程将消耗越来越多的资源。这会导致系统将花费更多时间来处理上下文的转换和线程管理,更少的时间来对连接服务,加入额外的线程实际上可能会增加客户端总服务时间。
解决:通过限制总线程数并重复使用线程来避免一客户一线程的缺陷。
2)使用线程池:与为每个线程创建新的线程不同,服务器在启动时创建固定数量的线程组成的线程池。当有一个客户端请求过来时,线程池将分配一个线程处理,线程在处理完请求后将会返回线程池,为下一次请求处理做好准备。如果连接请求到达服务器端,线程池中所有的线程都已被占用,它们则在一个队列中等待,直到有空闲的线程可用。
线程池服务端具体实现的步骤:
1.服务器端创建一个ServerSocket实例。
2.创建N个线程,每个线程都反复循环,从(共享的)ServerSocket实例中接收客户端连接。当多个线程同时调用同一个ServerSocket实例的accept()方法将会阻塞等待,直到一个新连接创建成功。
3.新建立连接对应Socket实例则只在选中的线程中返回。其他线程将阻塞,直到成功建立下一个连接和选中下一个幸运的线程。
缺点:创建的线程池太少,客户端可能等待很长时间才能获取服务,线程池大小不能根据客户端请求数量进行调整。
解决:Java中提供一个调度工具(系统管理调用接口Executor),可以在系统负载时扩展线程池的大小,负载较轻时缩减线程池的大小。
3)系统管理调度:接口Executor
Executor接口代表了一个根据某种策略来执行Runnable实例的对象,其中包含了排队和调度的细节,或者如何选择要执行的任务。Executor接口只定义了一个方法:
public interface Executor {
void execute(Runnable command);
}
Java中内置了大量的Executor接口实现,很简单使用,也可以进行扩展性配置。其中一些还提供了处理维护线程等繁琐细节的功能。ExecutorService接口继承于Executor接口,提供了一个更高级的工具来关闭服务器,包括正常关闭和突然关闭。ExecutorService还允许在完成任务后返回一个结果,这需要用到Callable接口,它和Runnable接口很像,只是多了一个返回值。
阻塞和超时
Socket的调用可能存多种原因而阻塞。
1)read(),receive()方法在没有数据可读时会阻塞。ServerSocket的accept()方法和Socket构造方法会阻塞等待,直到建立连接。
解决:使用socket,ServerSocket,以及DatagramSocket类的setSoTimeout()方法,来设置其阻塞的最长时间。指定时间内方法没有返回将会抛出异常InterruptedIOException。对于Socket实例,可以在read()方法前,在套接字的InputStream上调用available()方法检测是否有可读数据。
2)连接和写数据
Socket类的构造函数会尝试根据参数中指定的主机和端口来建立连接,并阻塞等待,直到连接成功建立或发生了系统定义的超时。不幸的是,系统定义的超时时间很长,而Java又没有提供任何缩短它的方法。要改变这种情况,可以使用Socket类的无参数构造函数,它返回的是一个没有建立连接的Socket实例。需要建立连接时,调用该实例的connect()方法,并指定一个远程终端和超时时间。
write()方法调用也会阻塞等待,直到最后一个字节成功写入到了TCP实现的本地缓存中。如果可用的缓存空间比要写入的数据小,在write()方法调用返回前,必须把一些数据成功传输到连接的另一端。
3)限制客户端的时间
通过服务期限和当前计算出截止时间,每次调用read()结束后,重新计算当前和截止时间的差值,即服务截止时间,并将套接字的超时时间设置为该剩余时间。
package com.tcp.ip.chapter4;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;
public class TimeLimitEchoProtocol implements Runnable {
private static final int BUFSIZE = 32;
private static final String TIMELIMIT = "10000";
private static final String TIMELIMITPROP = "Timelimit";
private static int timelimit;
private Socket clntSock;
private Logger logger;
public TimeLimitEchoProtocol(Socket clntSock, Logger logger){
this.clntSock = clntSock;
this.logger = logger;
timelimit = Integer.parseInt(System.getProperty(TIMELIMITPROP, TIMELIMIT));
}
public static void handleEchoClient(Socket clntSock, Logger logger){
try {
InputStream in = clntSock.getInputStream();
OutputStream out = clntSock.getOutputStream();
int recvMsgSize;
int totalBytesEchoed = 0;
byte[] echoBuffer = new byte[BUFSIZE];
//算出最后结束的时间
long endTime = System.currentTimeMillis() + timelimit;
int timeBoundMillis = timelimit;
clntSock.setSoTimeout(timeBoundMillis);
while ((timeBoundMillis > 0) &&
((recvMsgSize = in.read(echoBuffer)) != -1)) {
out.write(echoBuffer, 0, recvMsgSize);
totalBytesEchoed += recvMsgSize;
//当前剩余时间
timeBoundMillis = (int) (endTime - System.currentTimeMillis());
clntSock.setSoTimeout(timeBoundMillis);
logger.info("客户端 " + clntSock.getRemoteSocketAddress() +
", echoed " + totalBytesEchoed + " bytes.");
}
} catch (IOException ex) {
logger.log(Level.WARNING, "Exception in echo protocol" , ex);
}
}
public void run() {
handleEchoClient(this.clntSock, this.logger);
}
}
TCP多线程案例
客户端的代码
public class TCPEchoClient {
public static void main(String[] args) throws IOException {
Socket clientSocket = new Socket("127.0.0.1",1234);
//键盘录入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//发送到客户端
PrintStream out = new PrintStream(clientSocket.getOutputStream());
//读取从服务器端的回复
BufferedReader getBr = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String input = null;
while((input = br.readLine())!=null) {
out.println(input);
if("end".equals(input)) {
break;
}
String echoInfo = getBr.readLine();
System.out.println("Information from Server "+clientSocket.getRemoteSocketAddress()+": "+echoInfo);
}
clientSocket.close();
}
}
处理任务的类
public class EchoProtocol implements Runnable{
private Socket clientSocket;
private Logger logger;
public EchoProtocol(Socket socket ,Logger logger) {
this.clientSocket= socket;
this.logger = logger;
}
public static void handleEchoClient(Socket clientSocket,Logger logger) {
try {
//从客户端读取
BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//从客户端读取的信息,加echo+info进行回复
PrintStream out = new PrintStream(clientSocket.getOutputStream());
String info = null;
while((info =br.readLine())!=null) {
if("end".equals(info)) {
break;
}
System.out.println("information from client "+clientSocket.getRemoteSocketAddress()+": "+info);
out.println("echo:"+info);
}
clientSocket.close();
}catch(IOException e) {
}
}
@Override
public void run() {
handleEchoClient(clientSocket,logger);
}
}
1.一客户一线程的方式
public class TCPEchoServerThread {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(1234);
Logger logger = Logger.getLogger("practical");
while(true) {
Socket clientSocket = serverSocket.accept();
Thread thread = new Thread(new EchoProtocol(clientSocket,logger));
thread.start();
logger.info("Created and started Thread "+thread.getName());
}
}
}
2.线程池的方式
public class TCPEchoServerPool {
private static final int THREADPOOL = 5;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(1234);
Logger logger = Logger.getLogger("practical");
for(int i = 0; i < THREADPOOL;i++) {
Thread thread = new Thread() {
public void run() {
while(true) {
try {
Socket clientSocket = serverSocket.accept();
EchoProtocol.handleEchoClient(clientSocket, logger);
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
thread.start();
logger.info("Created and Started thread :"+thread.getName());
}
}
}
3.系统管理调度:Executor接口
public class TCPEchoServerExecutor {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(1234);
ExecutorService executor = Executors.newCachedThreadPool();
Logger logger = Logger.getLogger("pratical");
while(true) {
Socket clientSocket = socket.accept();
executor.submit(new EchoProtocol(clientSocket,logger));
}
}
}