proxyee源代码阅读

简介: 优雅的代理源代码阅读比较

代码仓库信息

仓库地址

https://github.com/monkeyWie/proxyee

星级:940

fork407

可以看到维护非常及时,最新的提交是2021/2/24。作者在issue中的回复也比较积极。

阅读分支

分支:master

commitb811b898ed21913fd6be14e048f5ad1a0ab48965

主要功能

proxyee是一个非常干净的二方库,总共只有2326行代码,依赖也仅有nettybcpkix-jdk15on(后者主要是为了支持SSL)。

主要功能是做proxy:

image.png

例如,引入该二方库依赖以后,我们在main函数中,写如下代码就可以启动一个proxy server

publicstaticvoidmain(String[] args) throws Exception {

HttpProxyServerConfig config = new HttpProxyServerConfig();
config.setHandleSsl(true);
new HttpProxyServer()
.serverConfig(config)
.proxyInterceptInitializer(new HttpProxyInterceptInitializer() {
@Override
public void init(HttpProxyInterceptPipeline pipeline) {
pipeline.addLast(new CertDownIntercept());
pipeline.addLast(new FullResponseIntercept() {
 
         
@Override
public boolean match(HttpRequest httpRequest, HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {
//在匹配到百度首页时插入js
return HttpUtil.checkUrl(pipeline.getHttpRequest(), "^www.baidu.com$")
&& HttpUtil.isHtml(httpRequest, httpResponse);
}
 
         
@Override
public void handleResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {
//打印原始响应信息
System.out.println(httpResponse.toString());
System.out.println(httpResponse.content().toString(Charset.defaultCharset()));
//修改响应头和响应体
httpResponse.headers().set("handel", "edit head");
/*int index = ByteUtil.findText(httpResponse.content(), "<head>");
ByteUtil.insertText(httpResponse.content(), index, "<script>alert(1)</script>");*/
httpResponse.content().writeBytes("<script>alert('hello proxyee')</script>".getBytes());
}
});
}
})
.start(9999);
}

启动以后,使用如下命令访问百度:

curl -H "accept:text/html" -k -x 127.0.0.1:9999 https://www.baidu.com

能看到百度的返回中包含如下字符:

image.png

总结一下:

  1. 实现proxy      server,后面我们能看到使用的是netty
  2. 支持定义拦截的逻辑。支持Basic验证。
  3. 允许定制请求和响应的修改类:雁过拔毛或者雁过加毛。在发送请求之前,修改请求(例如添加额外的请求header);在收到响应以后,修改响应内容,再返回给客户端。
  4. 支持SSL。本文对这部分先不做讨论。

 

如何使用

引入依赖:

<dependency>
<groupId>com.github.monkeywie</groupId>
<artifactId>proxyee</artifactId>
<version>1.4.1</version>
</dependency>

参考上面的代码就可以使用了。

准备知识

任何一个web proxy,本质上是扮演了两个角色:

  1. http服务器,用于接收请求。
  2. http客户端,用于把接收的请求代替别人发送出去。

启动服务器,接收请求,并帮助把请求发送出去,这些功能的实现,proxyee都依赖于netty

所以我们先科普一下netty是如何创建一个TCP服务器和发送TCP请求的,熟悉netty有助于帮忙我们理解proxyee本身的代码。

netty是一个NIO的异步网络编程框架,很多RPC服务(例如我们的HSF)都是使用的netty

Netty TCP Server

使用netty创建TCP Server的代码长这个样子:

// 1.
EventLoopGroup group = new NioEventLoopGroup();
 
         
try{
// 2.
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999));
 
         
// 3.
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new HelloServerHandler());
}
});
 
         
// 4.
ChannelFuture channelFuture = serverBootstrap.bind().sync();
channelFuture.channel().closeFuture().sync();
} catch(Exception e){
e.printStackTrace();
} finally {
group.shutdownGracefully().sync();
}

这上面的代码主要包含如下4个步骤:

  1. 创建一个EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();

我们可以这样理解EventLoopGroup:里面包含了一组线程,每一个线程都在不停地等待和处理IO事件,也就是接受TCP连接请求,并处理它们。Netty向我们保证,从一个TCP请求创建,到服务请求完成返回响应,都来自该线程组中某一个特定的线程。

  1. 创建一个ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(new InetSocketAddress("localhost", 9999));

bootstrap代表一个服务器实例,用于管理线程,绑定socket连接等。

首先,我们使用上面创建的EventLoopGroup来初始化它。

然后,设置服务器的channelNioServerSocketChannelChannelnetty的概念。当客户端通过TCP连接成功连接到服务器以后,一个Channel就会被创建,这里我们使用的Channel类型是NioServerSocketChannel,因为我们要用Java NIO来接受和响应请求。

最后,我们设置服务器的地址(localhost)和监听端口(9999)。

 

  1. 创建一个ChannelInitializer
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new HelloServerHandler());
}
});

创建ChannelInitializer并设置到ServerBootstrap上。ChannelInitializer主要工作是每当有新的TCP连接来的时 候,其initChannel方法都会被调用。通常来说,我们会在这里指定我们的请求处理器(Handler),例如我们该怎么解析消息体,如何返回响应等。

我们通过把这个handler加入到channelpipeline里面来发挥它的作用。

channelpipline,可以理解成Servlet里面的FilterChain。一个请求被读取以后,或者一个响应在发送之前,加入到pipline中的多个handler会对请求或者响应发生作用。

 

通常来说有两类handler

  • ChannelInboundHandler:专门处理来自客户端的请求,例如如果是一个http请求,可能你希望把他反序列化,拿到httpheaderbody等。
  • ChannelOutboundHandler:专门处理要发送给客户端的响应。

BTW,下面这个注释画的不错:

*                                                 I/O Request
*                                            via {@link Channel} or
*                                        {@link ChannelHandlerContext}
*                                                      |
*  +---------------------------------------------------+---------------+
*  |                           ChannelPipeline         |               |
*  |                                                  \|/              |
*  |    +---------------------+            +-----------+----------+    |
*  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  |               |                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  .               |
*  |               .                                   .               |
*  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
*  |        [ method call]                       [method call]         |
*  |               .                                   .               |
*  |               .                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  |               |                                  \|/              |
*  |    +----------+----------+            +-----------+----------+    |
*  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
*  |    +----------+----------+            +-----------+----------+    |
*  |              /|\                                  |               |
*  +---------------+-----------------------------------+---------------+
*                  |                                  \|/
*  +---------------+-----------------------------------+---------------+
*  |               |                                   |               |
*  |       [ Socket.read() ]                    [ Socket.write() ]     |
*  |                                                                   |
*  |  Netty Internal I/O Threads (Transport Implementation)            |

*  +-------------------------------------------------------------------+

出处:io.netty.channel.ChannelPipeline.java

  1. 启动服务。
ChannelFuture channelFuture = serverBootstrap.bind().sync();

最后一步,启动TCP服务。sync方法阻塞当前线程,直到服务完全启动。返回的对象是对Channel的一个封装,我们可以在该对象上添加各种服务级别事件的监听。

 

Netty TCP Client

我们也可以使用netty发送TCP请求。下面这段代码启动了一个TCP客户端。

// 1.
EventLoopGroup group = new NioEventLoopGroup();
try{
// 2.
Bootstrap clientBootstrap = new Bootstrap();
 
         
clientBootstrap.group(group);
clientBootstrap.channel(NioSocketChannel.class);
clientBootstrap.remoteAddress(new InetSocketAddress("localhost", 9999));
 
         
// 3.
clientBootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandler());
}
});
 
         
// 4.
ChannelFuture channelFuture = clientBootstrap.connect().sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}

上面的代码也主要分为4步。

  1. 创建一个EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();

这个和服务端创建是一样的。

  1. 创建和配置一个Bootstrap
Bootstrap clientBootstrap = new Bootstrap();

第二步我们创建一个TCP的客户端。服务端使用的是ServerBootstrap。这里我们使用的是Bootstrap。同时我们设置事件循环组Channel类型和远端服务器地址。

clientBootstrap.group(group);
clientBootstrap.channel(NioSocketChannel.class);
clientBootstrap.remoteAddress(new InetSocketAddress("localhost", 9999));

 

  1. 创建一个ChannelInitializer
clientBootstrap.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ClientHandler());
}
});

ChannelInitializer在每次TCP连接建立的时候,其initChannel都会被调用。上面的代码中,当发起TCP请求的时候,我们添加一个ClientHandler用于处理TCP请求的发送。

同时我们将ChannelInitializer设置为客户端clientBootstraphandler

4. 启动客户端

最后一步,我们的客户端需要连接远端服务器,

ChannelFuture channelFuture = clientBootstrap.connect().sync();

该行代码同样返回一个ChannelFuture

然后我们可以关闭client

channelFuture.channel().closeFuture().sync();

 

代码阅读

源码结构

proxyee是一个单modulemaven项目,test目录并未包含单测,而是每个class都包含一个可运行的main方法,用于演示如何实现各种proxy的功能。

main目录是这样的:

image.png

其中handler目录下,

  • HttpProxyServerHandler是一个ChannelInboundHandlerAdapter(即ChannelInboundHandler),实现了proxy服务端的功能。
  • HttpProxyClientHandler也是一个ChannelInboundHandler,实现了proxy作为客户端的功能。

这两个是proxyee的核心类。

服务启动

下面是proxyee启动服务的代码。

new HttpProxyServer()
.serverConfig(config)
.proxyInterceptInitializer(new HttpProxyInterceptInitializer() {
@Override
public void init(HttpProxyInterceptPipeline pipeline) {
pipeline.addLast(new CertDownIntercept());
pipeline.addLast(new FullResponseIntercept() {
@Override
public void handleResponse(HttpRequest httpRequest, FullHttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) {
ByteUtil.insertText(httpResponse.content(), index, "<script>alert(1)</script>");*/
httpResponse.content().writeBytes("<script>alert('hello proxyee')</script>".getBytes());
}
});
}
}).start(9999);

很类似netty服务端启动时添加handler的方式:

 
         
// 3. netty服务端添加IO handler
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new HelloServerHandler());
}
});

我们可以看到在proxyee源码中,作者参考了nettySDK设计的风格。

proxyeeHttpProxyInterceptInitializer类似nettyChannelInitializer的作用,在每次请求建立的时候,HttpProxyInterceptInitializerinit(HttpProxyInterceptPipeline pipeline)方法都会被调用,用户可以在此方法中趁机添加IO处理的类。

我们从HttpProxyServerstart方法进入,看看启动一个proxy server的流程。

publicvoidstart(String ip, int port) {

// 1.
init();
 
         
// 2.
bossGroup = new NioEventLoopGroup(serverConfig.getBossGroupThreads());
workerGroup = new NioEventLoopGroup(serverConfig.getWorkerGroupThreads());
try {
// 3.
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
// 4.
.childHandler(new ChannelInitializer<Channel>() {
 
         
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
ch.pipeline().addLast("serverHandle",
new HttpProxyServerHandler(serverConfig, proxyInterceptInitializer, tunnelIntercept, proxyConfig,
httpProxyExceptionHandle));
}
});
ChannelFuture f;
if (ip == null) {
f = b.bind(port).sync();
} else {
f = b.bind(ip, port).sync();
}
f.channel().closeFuture().sync();
} catch (Exception e) {
httpProxyExceptionHandle.startCatch(e);
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
  1. init()

主要是初始化和SSL证书相关的操作。

  1. 创建父子事件循环组。
bossGroup = new NioEventLoopGroup(serverConfig.getBossGroupThreads());
workerGroup = new NioEventLoopGroup(serverConfig.getWorkerGroupThreads());

Netty服务端启动时候,我们可以使用父子事件循环组,其中bossGroup专门用于监听客户端的连接事件,连接成功以后,workerGroup则会负责连接中发生的IO事件。

  1. Netty服务端初始化。

这部分几乎没有什么可以说的,完全是Netty标准做法。忘记了的同学可以回去参考一下“Netty TCP Server”那一节。

  1. childHandler的设置。

值得注意的是他添加的自定义Netty IO handler

.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
// 4.1
ch.pipeline().addLast("httpCodec", new HttpServerCodec());
// 4.2
ch.pipeline().addLast("serverHandle",
new HttpProxyServerHandler(serverConfig, proxyInterceptInitializer, tunnelIntercept, proxyConfig,
httpProxyExceptionHandle));
}
});

首先,添加了一个httpCodechandler。这是Netty自带的提供http服务的handler。它是一个全双工”handler:既是一个InHandler,也是一个outHandler,负责把接收到的二进制请求反序列化成Http Request对象,并序列化Http Response

然后,添加了一个“serverHandler”,类型是HttpProxyServerHandler,它也继承了NettyChannelInboundHandlerAdapter,所以它是一个处理IO请求的拦截器

然后启动流程就做完了,是不是非常简单?

接下来,我们看一下在HttpProxyServerHandler中,proxyee做了什么操作。其实我们可以猜测一下,前面说到一个proxyee是需要扮演服务器和客户端的角色的,那么HttpProxyServerHandler里面的主要逻辑应该包括两个:

  1. 接受来自客户的请求。
  2. 代替客户发送请求,收到响应以后再返回给客户。

服务端请求的接受

HttpProxyServerHandler继承ChannelInboundHandlerAdapter以后,主要处理http请求的逻辑在方法channelRead中体现。

这里首先要插一句:HttpProxyServerHandler的生命周期是什么样的?

我们可以在HttpProxyServerHandler构造函数中断点,然后发送curl请求。发现每次发送请求都会新构造一个该类的实例。考虑Netty事件循环的特点,所以该类的实例变量是线程安全的。

image.png

回到channelRead方法。

@Override

publicvoidchannelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {

if (msg instanceof HttpRequest) {
// ...
 
         
interceptPipeline = buildPipeline();
interceptPipeline.setRequestProto(new RequestProto(host, port, isSsl));
 
         
// ...
interceptPipeline.beforeRequest(ctx.channel(), request);
} else if (msg instanceof HttpContent) {
 
         
// ...
interceptPipeline.beforeRequest(ctx.channel(), (HttpContent) msg);
 
         
}
// ...
}

 

这个方法主要在做两件事。

  1. 如果它接收到的msg对象是一个HttpRequest类型,那么初始化proxyeepipeline,同时依次调用pipeline中定义的各拦截器的beforeRequest(HttpRequest      msg)方法。
  1. HttpRequestNetty暴露的一个类,包含Http请求的请求头。
  1. 否则,如果接收到的msg对象是一个HttpContent类型,那么依次调用pipeline中定义的各拦截器的beforeRequest(HttpContent      msg)
  1. HttpContentNetty暴露的一个类,包含Http请求的请求体。
  2. 2发生以后,channelRead收集到客户端的请求头和请求体了,这时候代替客户,发送请求到目标服务器。

 

注意到,12是互斥的两个操作,也就是说channelRead根据入参msg(类型是一个Object),要么做1,要么做2。所以我们知道channelRead一定是会被调用两次的,一次传入请求头,一次传入请求体。

 

小知识

Netty的这个行为和HttpServerCodec的实现有关。在其父类ByteToMessageDecoder中,解码Http请求以后,channelRead方法会得到请求头和请求体两个msg实体然后对两个实体分别触发Netty pipeline下游的其他IO handler

 

参考:io.netty.handler.codec.ByteToMessageDecoder 322

static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {

  for (int i = 0; i < numElements; i ++) {

      ctx.fireChannelRead(msgs.getUnsafe(i));
  }

}

 

我们前面说了,Netty本身在IO请求过程中,也是使用的pipeline的模式(敲黑板:设计模式之责任链)。proxyee显然也参考了该设计模式,构建了一个pipeline来允许proxyee用户自定义拦截器。

到此为止,我们知道proxyee作为服务器角色的逻辑,但是我们还不清楚它是如何扮演客户端角色,帮助转发请求的。

请求的流动

我们看一下在proxyeepipeline中都“build”进去哪些拦截器。

interceptPipeline = buildPipeline();

interceptPipeline的类型HttpProxyInterceptPipeline,实现了Iterable接口,用于遍历HttpProxyIntercept。每一个HttpProxyIntercept有如下方法:

/**
* 拦截代理服务器到目标服务器的请求头
*/
public void beforeRequest(Channel clientChannel, HttpRequest httpRequest,

                             HttpProxyInterceptPipeline pipeline);

 
         
/**
* 拦截代理服务器到目标服务器的请求体
*/
public void beforeRequest(Channel clientChannel, HttpContent httpContent,

                             HttpProxyInterceptPipeline pipeline);

 
         
/**
* 拦截代理服务器到客户端的响应头
*/
public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse,

                             HttpProxyInterceptPipeline pipeline);

 
         
/**
* 拦截代理服务器到客户端的响应体
*/
public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent,

                             HttpProxyInterceptPipeline pipeline);

在构建proxyee pipeline代码中,proxyee首先加入了一个默认处理器。

new HttpProxyIntercept() {
@Override
public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline)

    throws Exception {

handleProxyData(clientChannel, httpRequest, true);
}
 
         
@Override
public void beforeRequest(Channel clientChannel, HttpContent httpContent, HttpProxyInterceptPipeline pipeline)

                   throws Exception {

handleProxyData(clientChannel, httpContent, true);
}
 
         
@Override
public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse,

                                     HttpProxyInterceptPipeline pipeline) throws Exception {

clientChannel.writeAndFlush(httpResponse);
if (HttpHeaderValues.WEBSOCKET.toString().equals(httpResponse.headers().get(HttpHeaderNames.UPGRADE))) {
// websocket转发原始报文
proxyChannel.pipeline().remove("httpCodec");
clientChannel.pipeline().remove("httpCodec");
}
}
 
         
@Override
public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent,

                                     HttpProxyInterceptPipeline pipeline) throws Exception {

clientChannel.writeAndFlush(httpContent);
}
}

这个默认拦截器始终放在HttpProxyInterceptPipeline的队尾,其他用户自定义的拦截器前置于它。

当在channelRead方法中调用:

interceptPipeline.beforeRequest(ctx.channel(), request);

的时候,interceptPipeline会以递归的方式,依次调用其添加拦截器,直到遍历到最后一个,也即如上的默认拦截器。

publicvoidbeforeRequest(Channel clientChannel, HttpRequest httpRequest) throws Exception {

this.httpRequest = httpRequest;
if (this.posBeforeHead < intercepts.size()) {
HttpProxyIntercept intercept = intercepts.get(this.posBeforeHead++);
intercept.beforeRequest(clientChannel, this.httpRequest, this);
}
this.posBeforeHead = 0;
}

递归的部分比较巧妙,

intercept.beforeRequest(clientChannel, this.httpRequest, this);

会在执行完自己的逻辑以后,再次调用interceptPipeline(通过第三个参数this传递)的beforeRequest,从而继续进入下一个intercept的调用。这个和NettyfireChannelEvent类似,也类似Servlet中的FilterChain

我们关注最后一个默认的intercept,其中处理request的部分的handleProxyData方法。

publicvoidbeforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline)

    throws Exception {

handleProxyData(clientChannel, httpRequest, true);
}

方法实现:

privatevoidhandleProxyData(Channel channel, Object msg, boolean isHttp) throws Exception {

if (cf == null) {
// ...
requestProto = interceptPipeline.getRequestProto();
HttpRequest httpRequest = (HttpRequest) msg;
 
         
//..
ChannelInitializer channelInitializer = isHttp ? new HttpProxyInitializer(channel, requestProto, proxyHandler)
: new TunnelProxyInitializer(channel, proxyHandler);
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(serverConfig.getProxyLoopGroup()) // 注册线程池
.channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
.handler(channelInitializer);
if (proxyHandler != null) {
// 代理服务器解析DNS和连接
bootstrap.resolver(NoopAddressResolverGroup.INSTANCE);
} else {
bootstrap.resolver(serverConfig.resolver());
}
requestList = new LinkedList();
cf = bootstrap.connect(requestProto.getHost(), requestProto.getPort());
cf.addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
future.channel().writeAndFlush(msg);
synchronized (requestList) {
requestList.forEach(obj -> future.channel().writeAndFlush(obj));
requestList.clear();
isConnect = true;
}
} else {
requestList.forEach(obj -> ReferenceCountUtil.release(obj));
requestList.clear();
getExceptionHandle().beforeCatch(channel, future.cause());
future.channel().close();
channel.close();
}
});
} else {
synchronized (requestList) {
if (isConnect) {
cf.channel().writeAndFlush(msg);
} else {
requestList.add(msg);
}
}
}
}

handleProxyData也主要做了两件事:

  1. 如果是首次调用,创建并初始化转发请求使用channle,连接远端目标服务。流程和“Netty TCP Client”描述的一样。
  2. 如果不是首次调用,把请求的对象加入到requestList列表中。待连接恢复,依次发送列表中暂存的请求对象。

注意由于在1中,连接成功发送请求并清空requestList,并不是在当前线程(future listener),所以requestList的修改需要同步锁。

到此为止,我们coverproxyee的主要流程。总结一下:

image.png

 

作者真是玩套娃的好手。

总结

proxyee是一款基于Netty的开源代理二方库,代码精简,易于学习,同时也能帮助大家理解NettySDK行为。

参考文档

 

目录
相关文章
|
4月前
|
Linux Shell 数据库
【绝技大公开】Linux文件查找新招式:打破常规,探索那些鲜为人知的技巧,让你成为真正的文件追踪大师!
【8月更文挑战第13天】文件查找是Linux用户必备技能,能大幅提升工作效率。本文介绍几种高效查找方法,包括使用`column`美化`find`输出、利用`locate`和`mlocate`快速搜索、编写脚本自动化任务、采用`fd`现代工具提升查找体验,以及结合`grep`和`rg`搜索文件内容。此外,还推荐了`Gnome Search Tool`和`Albert`等图形界面工具,为用户提供多样选择。掌握这些技巧,让你的工作更加得心应手。
52 2
|
安全 Linux 编译器
linux下c语言内存检测神器asan,专治各种疑难杂症
linux下c语言内存检测神器asan,专治各种疑难杂症
linux下c语言内存检测神器asan,专治各种疑难杂症
|
设计模式 网络协议 前端开发
proxyee源代码阅读
优雅的代理源代码阅读比较
214 0
proxyee源代码阅读
|
安全 Android开发
21天打卡Andoid学到的一些小知识-第十九二十二十一天
今天我们学习打卡的内容是:android 10.0 去掉未知来源弹窗 默认授予安装未知来源权限
95 0
|
设计模式 前端开发 算法
程序员提高之源代码阅读篇
前言 最近刚换了新工作,正在熟悉公司环境,因此博客更新有所耽误,那么本篇也是选自入职部门分享的主题,从今年5月份开始,我也在阅读 Spring 的源码,参考网络上的内容以及本人学习的一些经验,总结出本篇。
131 0
|
Oracle 关系型数据库 Linux
Linux环境Oracle安装大全,呕心狂敲万字,绝对提升你的视野
Linux环境Oracle安装大全,呕心狂敲万字,绝对提升你的视野
256 0
Linux环境Oracle安装大全,呕心狂敲万字,绝对提升你的视野
|
Web App开发 JSON 程序员
吐血推荐 | 珍藏多年的 Chrome 插件,务必收藏
熟话说,工欲善其事,必先利其器,Chrome 作为程序员使用最多的浏览器有着数不清的优点,简洁高效,强大的控制面板,支持各种插件等。当然也有一个一直被我们吐槽的缺点,就是内存占用高,好在现在硬件便宜,可是说是瑕不掩瑜。今天就给大家推荐一些自己常用的 Chrome 插件,让你的开发效率和逼格都提升一个档次。
157 0
吐血推荐 | 珍藏多年的 Chrome 插件,务必收藏
|
C# 数据库 Windows
艾伟:基于.NET平台的Windows编程实战(一)——前言
本系列文章导航 基于.NET平台的Windows编程实战(一)——前言 基于.NET平台的Windows编程实战(二)—— 需求分析与数据库设计 基于.NET平台的Windows编程实战(四)—— 数据库操作类的编写 基于.NET平台的Windows编程实战(五)—— 问卷管理功能的实现 基于.NET平台的Windows编程实战(六)—— 题目管理功能的实现   前言:本系列文章是一个关于.NET Windows编程的入门实战教程。
809 0
|
C# 数据库 Windows
艾伟_转载:基于.NET平台的Windows编程实战(一)——前言
本系列文章导航 基于.NET平台的Windows编程实战(一)——前言 基于.NET平台的Windows编程实战(二)—— 需求分析与数据库设计 基于.NET平台的Windows编程实战(四)—— 数据库操作类的编写 基于.NET平台的Windows编程实战(五)—— 问卷管理功能的实现 基于.NET平台的Windows编程实战(六)—— 题目管理功能的实现   前言:本系列文章是一个关于.NET Windows编程的入门实战教程。
1032 0
|
定位技术