用Java自制一个代理服务器,全自己写的代码...道理简单,但有些细节需要注意,否则会踩坑!
需要注意的细节有3个,看看前面的注释就有说了.
/**
* 代理服务器的线程.传入一个ServerSocket,该线程将监听并处理.
* 监听后立即新建另一个相同的线程,重新监听.
*/
package setycyas.proxy;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* @author setycyas 2018-08-10
*
* 基本完成的代理服务器,运行静态main即可.
* 原理简单,用socket实现,几乎是直接交换数据.注意重点:
*
* 1.https请求时,首个请求是connect,不能直接发数据去服务器,需要先回应:
* "HTTP/1.1 200 Connection Established\r\n\r\n",不能漏换行!我就这里踩了坑.
*
* 2.单线程不行,需要2个,一个把客户端的输入交给服务器,另一个把服务器输入交个客户端.
*
* 3.监听成功后,立即新建线程继续监听.因为只有一个线程能监听,所以不能同时开多个监听的.
* 只有在自己的监听成功后,才能开新的.要是不开,代理服务器就无法响应新请求了.
* 所以每次客户端的都需要新线程.
*
* 要注意的重点只有这3个.但细节还是很耐写的.
*
* 另外,阻塞中关闭线程的方法,看:https://blog.csdn.net/al_assad/article/details/52992546
* 这里关键是流的读写阻塞很多,把流关闭了,就一切清净.这是让所有线程通过异常终止的最简单办法!
*/
public class ProxyThread extends Thread{
/* 静态常量 */
// 测试记录文件夹
private static final String TESTFOLDER
= "D:/MyDocument/MyCode/EclipseWorkspace/JavaTest/TestFiles/proxyRequests/";
// 测试用端口默认值
private static final int TESTPORT = 2234;
// 读取客户端的buffer的默认大小
private static final int CLIENTBUFFER_SIZE = 4*1024;
// 读取客户端的buffer的默认大小
private static final int SERVERBUFFER_SIZE = 4*1024;
// 服务器默认端口
private static final int SERVERDEFAULT_PORT = 80;
/* 静态变量 */
// 所有子线程的集合,最终关闭所有时使用
private static HashSet<Thread> proxyThreadSet = new HashSet<Thread>();
/* 实例变量 */
// 首次访问方法,暂时只考虑是否connect
String firstMethod = "";
// 服务器port
int serverPort = -1;
// 服务器主机名
String serverHost = "";
// 线程id,输出时识别用
public final int id;
// 线程名称,跟id相关,在构造函数初始化,文字输出用
public final String name;
// 终止符
boolean stopFlag = false;
// 代理服务器的serverSocket
private ServerSocket ss = null;
// 对客户端的socket
private Socket client = null;
// 对服务器的socket
private Socket server = null;
// 读入客户端消息的buffer
byte[] clientReadBuffer = new byte[CLIENTBUFFER_SIZE];
// 从客户端上次读入的信息长度
int clientBytesRead = -1;
// 读入客户端消息的buffer
byte[] serverReadBuffer = new byte[SERVERBUFFER_SIZE];
// 从客户端上次读入的信息长度
int serverBytesRead = -1;
// 与客户端交互的stream
InputStream clientInputStream = null;
OutputStream clientOutputStream = null;
// 与服务器交互的stream
InputStream serverInputStream = null;
OutputStream serverOutputStream = null;
// 记录client传送消息的文件名与输出stream
private String clientRecordFile = null;
private FileOutputStream clientRecordOutputStream = null;
// 记录从client与server读取的总字节数
int clientTotalRead = 0;
int serverTotalRead = 0;
/* 实例公有变量 */
// 文字编码,随意修改,解码失败就抛异常
public String charSet = "UTF-8";
/* 构造函数,初始化各种变量 */
public ProxyThread(ServerSocket ss, int id, String recordFolder) {
super();
this.ss = ss;
if(id > 1 ) {
this.id = id;
}else {
this.id = 1;
}
this.name = "ProxyThread-"+this.id;
this.clientRecordFile = recordFolder+name+"clientInputRecord";
File fout = new File(clientRecordFile);
try {
if(!fout.exists())
fout.createNewFile();
this.clientRecordOutputStream = new FileOutputStream(fout);
}catch(Exception e) {
this.stopFlag = true;
e.printStackTrace();
}
}
public ProxyThread(ServerSocket ss, int id) {
this(ss, id, TESTFOLDER);
}
/* 把一个字节数组按长度变成字符串,注意编码定义在类的公有变量中 */
private String stringFromBytes(byte[] bs, int bsLen) throws UnsupportedEncodingException {
if(bs.length > bsLen) {
byte[] newBs = new byte[bsLen];
for(int i = 0;i < bsLen;i++)
newBs[i] = bs[i];
return new String(newBs, charSet);
}else {
return new String(bs, charSet);
}
}
/* 处理主机与端口组合的字符串,获取主机与端口 */
private void handleHostAndPortString(String hostAndPortString) {
String str = hostAndPortString.trim().toLowerCase();
String[] strSplit = str.split(":");
this.serverHost = strSplit[0];
if (strSplit.length > 1) {
this.serverPort = Integer.parseInt(strSplit[1]);
}else {
this.serverPort = SERVERDEFAULT_PORT;
}
return;
}
/* 处理首次访问的字节数组,获取访问方法,主机,端口 */
private void handleFirstBytes(byte[] bs,int bsLen) {
try {
// 主机与端口组合的字符串,预定义,将来用于分析
String hostAndPort = null;
// 变成字符串,按换行分割.
String src = stringFromBytes(bs, bsLen).toLowerCase();
String[] lines = src.split("\n");
String firstLine = lines[0];
String[] firstLineSplit = firstLine.split(" ");
// 获取首次访问方法
this.firstMethod = firstLineSplit[0];
// 获取主机与端口字符串.考虑Connect方法,再考虑非Connect方法
if (this.firstMethod.equals("connect")) {
hostAndPort = firstLineSplit[1];
}else {
for(int i = 0;i < lines.length;i++) {
if(lines[i].startsWith("host")) {
hostAndPort=lines[i].split(":",2)[1].trim();
break;
}
}
}
// 处理hostAndPort字符串
this.handleHostAndPortString(hostAndPort);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.err.println(name+"客户端首次Input解码失败!");
this.stopFlag = true;
}
return;
}
/* 处理客户端的首次输入,建立与客户端的连接,然后建立与服务器的连接.
* 如果是CONNECT方法,建立对服务器的连接,并向客户端返回:
* "HTTP/1.1 200 Connection Established\r\n\r\n",不能漏换行!
* 如果不是CONNECT(功能不全,直接当作GET),建立连接,客户端的首次输入也要发送给服务器.
*/
private void handleFirstClientInput() throws IOException {
// 建立与客户端的连接
this.clientInputStream = client.getInputStream();
this.clientOutputStream = client.getOutputStream();
System.out.println(name+"首次接收client数据,成功获取client的InputStream与OutputStream");
// 读取一次客户端数据
clientBytesRead = clientInputStream.read(clientReadBuffer);
this.clientTotalRead += clientBytesRead;
this.clientRecordOutputStream.write(clientReadBuffer, 0, clientBytesRead);
System.out.println(name+"首次接收的client数据已写入记录!");
// 处理第一次获取的数据,获取访问方法,服务器主机,端口.这些都在类的变量中保存.
this.handleFirstBytes(clientReadBuffer, clientBytesRead);
// 与服务器建立连接
this.server = new Socket(this.serverHost,this.serverPort);
this.serverOutputStream = server.getOutputStream();
this.serverInputStream = server.getInputStream();
System.out.println("连接到Server - "+serverHost+":"+serverPort);
// 如果是connect方法首次访问,向客户端回应OK.
// 否则,直接向服务器发送首次请求的数据
if(this.firstMethod.equals("connect")) {
String reply = "HTTP/1.1 200 Connection Established\r\n\r\n";
this.clientOutputStream.write(reply.getBytes());
System.out.println("客户端的首次访问方法是Connect,已作出响应.");
}else {
this.serverOutputStream.write(clientReadBuffer, 0, clientBytesRead);
}
// 完成
return;
}
@SuppressWarnings("deprecation")
@Override
public void run() {
try {
System.out.println(name+" start to run");
proxyThreadSet.add(this);
if (stopFlag) {
System.out.println(name+"构造函数出了问题,终止");
proxyThreadSet.remove(this);
return;
}
// 预定义一个接收客户端输入的线程,将来中断用.
ServerInputStreamTransfer serverInputStreamTransfer = null;
try {
this.client = ss.accept();
// 监听成功后,开一个新线程监听下一个客户端访问
(new ProxyThread(ss,id+1)).start();
// 处理第一次得到的客户端数据
handleFirstClientInput();
System.out.println(name+"处理客户端首次输入完成!");
// 启动读取服务器数据的线程,而读取客户端数据将在本线程执行.
// 理由简单:客户端输入决定代理服务器的行为,更重要,在主线程处理.
System.out.println(name+"开启读取server输入的新线程!");
serverInputStreamTransfer = new ServerInputStreamTransfer(this);
proxyThreadSet.add(serverInputStreamTransfer);
serverInputStreamTransfer.start();
// 当前线程读取client的输入
while((clientBytesRead = clientInputStream.read(clientReadBuffer)) != -1) {
this.serverOutputStream.write(clientReadBuffer, 0, clientBytesRead);
this.clientTotalRead += clientBytesRead;
this.clientRecordOutputStream.write(clientReadBuffer, 0, clientBytesRead);
if (this.stopFlag) break;
}
} catch (Exception e) {
e.printStackTrace();
System.err.println(name+"出现错误!");
}finally {
System.out.println(name+"准备结束!");
// 分析结束原因,关闭子线程
if (!this.stopFlag) {
System.out.println(name+"的结束是由client连接断开引起的");
if (serverInputStreamTransfer.isAlive()) {
serverInputStreamTransfer.stop();
}
}else {
if (serverInputStreamTransfer.isAlive()) {
System.out.println(name+"的结束是由外部命令引起");
serverInputStreamTransfer.stop();
}else {
System.out.println(name+"的结束是由server连接断开");
}
}
// 结束:关闭所有流,终止server读取线程
try {
this.serverInputStream.close();
this.clientInputStream.close();
this.serverOutputStream.close();
this.clientOutputStream.close();
this.clientRecordOutputStream.close();
}catch(Exception e) {
e.printStackTrace();
System.err.println("关闭流的过程出现错误!");
}
}
proxyThreadSet.remove(this);
proxyThreadSet.remove(serverInputStreamTransfer);
System.out.println(name+"结束!!!");
System.out.println(name+"统计:从客户端读取字节数: "+this.clientTotalRead
+" 从服务器读取字节数: "+this.serverTotalRead);
}catch(Exception e) {
e.printStackTrace();
}
}
/**
* 入口main函数
* @throws IOException
*/
@SuppressWarnings("resource")
public static void main(String[] args) {
// TODO 基本完成,测试中
int port = TESTPORT;
ProxyThread pt = null;
ServerSocket ss = null;
int threadId = 888;
try {
ss = new ServerSocket(port);
pt = new ProxyThread(ss,threadId);
pt.start();
} catch (IOException e) {
e.printStackTrace();
}
// 监听键盘对控制台的输入,一旦有回车输入,终止代理服务器!这样所有线程的读写流都关闭了,
// 所有线程都将异常终止.是关闭主线程的最好方法.
(new Scanner(System.in)).nextLine();
try {
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("主线程结束!!!");
}
}
/* 把server的input数据往client的output传送的子线程,需要主线程数据支持 */
class ServerInputStreamTransfer extends Thread{
private ProxyThread proxy = null;
ServerInputStreamTransfer(ProxyThread proxy){
super();
this.proxy = proxy;
}
@Override
public void run() {
InputStream ins = proxy.serverInputStream;
OutputStream outs = proxy.clientOutputStream;
byte[] buffer = proxy.serverReadBuffer;
if ((ins == null) || (outs == null)) {
System.err.println(proxy.name+"创建server读取线程时,输入输出流为空!");
return;
}
// 不断读取server的输入流,交给客户端
try {
while((proxy.serverBytesRead = ins.read(buffer)) != -1) {
outs.write(buffer, 0, proxy.serverBytesRead);
proxy.serverTotalRead += proxy.serverBytesRead;
if (proxy.stopFlag) break;
}
}catch(IOException e) {
e.printStackTrace();
}
proxy.stopFlag = true;
}
}