这种问题怎么解决父母问题

什么样的RPC才是好用的RPC - 企业架构 - ITeye资讯
相关知识库:
本文来自:
作者:
现在RPC框架很多,但是真正好用的RPC却是少之又少。那么什么是好用的RPC,什么是不好用的RPC呢,有一个评判标准吗?下面是我列举出来的衡量RPC好用与否的几条标准:
引用真的像本地函数一样调用
使用简单,用户只需要关注业务即可
灵活,RPC调用的序列化方式可以自由定制,比如支持json,支持msgpack等方式
下面来分别解释这几条标准。
标准1:真的像本地函数一样调用
RPC的本质是为了屏蔽网络的细节和复杂性,提供易用的api,让用户就像调用本地函数一样实现远程调用,所以RPC最重要的就是“像调用本地函数一样”实现远程调用,完全不让用户感知到底层的网络。真正好用的RPC接口,他的调用形式是和本地函数无差别的,但是本地函数调用是灵活多变的。服务器如果提供和客户端完全一致的调用形式将是非常好用的,这也是RPC框架的一个巨大挑战
标准2:使用简单,用户只需要关注业务即可
RPC的使用简单直接,非常自然,就是和调用本地函数一样,不需要写一大堆额外代码,用户只用写业务逻辑代码,而不用关注框架的细节,其他的事情都由RPC框架完成。
标准3:灵活,RPC调用的序列化方式可以自由定制
RPC调用的数据格式支持多种编解码方式,比如一些通用的json格式、msgpack格式或者boost.serialization等格式,甚至支持用户自己定义的格式,这样使用起来才会更灵活。
RPC框架评估
下面根据这几个标准来评估一些国内外知名大公司的RPC框架,这些框架的用法在github的wiki中都有使用示例,使用示例代码均来自官方提供的例子。
gRPC最近发布了1.0版本,他是谷歌公司用c++开发的一个RPC框架,并提供了多种客户端。
先定义一个.proto的文件,例如
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
定义了一个服务接口,接收客户端传过来的Point,返回一个Feature,接下来定义protocol buffer的消息类型,用于序列化/反序列化
message Point {
int32 latitude = 1;
int32 longitude = 2;
服务器代码
class RouteGuideImpl final : public RouteGuide::Service {
Status GetFeature(ServerContext* context, const Point* point, Feature* feature) override {
feature-&set_name(GetFeatureName(*point, feature_list_));
feature-&mutable_location()-&CopyFrom(*point);
return Status::OK;
void RunServer(const std::string& db_path) {
std::string server_address("0.0.0.0:50051");
RouteGuideImpl service(db_path);
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr&Server& server(builder.BuildAndStart());
std::cout && "Server listening on " && server_address && std::
server-&Wait();
客户端代码
bool GetOneFeature(const Point& point, Feature* feature) {
Status status = stub_-&GetFeature(&context, point, feature);
if (!status.ok()) {
std::cout && "GetFeature rpc failed." && std::
if (!feature-&has_location()) {
std::cout && "Server returns incomplete feature." && std::
gRPC调用的序列化用的是protocal buffer,RPC服务接口需要在.proto文件中定义,使用稍显繁琐。根据标准1,gRPC并没有完全实现像本地调用一样,虽然很接近了,但做不到,原因是RPC接口中必须带一个Context的参数,并且返回类型必须是Status,这些限制导致gRPC无法做到像本地接口一样调用。
根据标准2,gRPC的使用不算简单,需要关注诸多细节,比如Context和Status等框架的细节。根据标准3,gRPC只支持pb协议,无法扩展支持其他协议。
综合评价:70分。
百度sofa-pbRPC
sofa-pbRPC是百度用c++开发的一个RPC框架,和gRPC有点类似,也是基于protocal buffer的,需要定义协议。
// 定义请求消息
message EchoRequest {
required string message = 1;
// 定义回应消息
message EchoResponse {
required string message = 1;
// 定义RPC服务,可包含多个方法(这里只列出一个)
service EchoServer {
rpc Echo(EchoRequest) returns(EchoResponse);
服务器端代码
#include &sofa/pbrpc/pbrpc.h&
// sofa-pbrpc头文件
#include "echo_service.pb.h"
// service接口定义头文件
class EchoServerImpl : public sofa::pbrpc::test::EchoServer
EchoServerImpl() {}
virtual ~EchoServerImpl() {}
virtual void Echo(google::protobuf::RpcController* controller,
const sofa::pbrpc::test::EchoRequest* request,
sofa::pbrpc::test::EchoResponse* response,
google::protobuf::Closure* done)
sofa::pbrpc::RpcController* cntl =
static_cast&sofa::pbrpc::RpcController*&(controller);
SLOG(NOTICE, "Echo(): request message from %s: %s",
cntl-&RemoteAddress().c_str(), request-&message().c_str());
response-&set_message("echo message: " + request-&message());
done-&Run();
服务完成后必须调用done-&Run(),通知RPC系统服务完成,触发发送Response;
在调了done-&Run()之后,Echo的所有四个参数都不再能访问;
done-Run()可以分派到其他线程中执行,以实现了真正的异步处理;
客户端代码
int main()
SOFA_PBRPC_SET_LOG_LEVEL(NOTICE);
// 定义RpcClient对象,管理RPC的所有资源
// 通常来说,一个client程序只需要一个RpcClient实例
// 可以通过RpcClientOptions指定一些配置参数,譬如线程数、流控等
sofa::pbrpc::RpcClientOptions client_
client_options.work_thread_num = 8;
sofa::pbrpc::RpcClient rpc_client(client_options);
// 定义RpcChannel对象,代表一个消息通道,需传入Server端服务地址
sofa::pbrpc::RpcChannel rpc_channel(&rpc_client, "127.0.0.1:12321");
// 定义EchoServer服务的桩对象EchoServer_Stub,使用上面定义的消息通道传输数据
sofa::pbrpc::test::EchoServer_Stub stub(&rpc_channel);
// 定义和填充调用方法的请求消息
sofa::pbrpc::test::EchoR
request.set_message("Hello world!");
// 可以通过RpcClientOptions指定一些配置参数,譬如线程数、流控等
sofa::pbrpc::RpcClientOptions client_
client_options.work_thread_num = 8;
sofa::pbrpc::RpcClient rpc_client(client_options);
// 定义RpcChannel对象,代表一个消息通道,需传入Server端服务地址
sofa::pbrpc::RpcChannel rpc_channel(&rpc_client, "127.0.0.1:12321");
// 定义EchoServer服务的桩对象EchoServer_Stub,使用上面定义的消息通道传输数据
sofa::pbrpc::test::EchoServer_Stub stub(&rpc_channel);
// 定义和填充调用方法的请求消息
sofa::pbrpc::test::EchoR
request.set_message("Hello world!");
// 定义方法的回应消息,会在调用返回后被填充
sofa::pbrpc::test::EchoR
// 定义RpcController对象,用于控制本次调用
// 可以设置超时时间、压缩方式等;默认超时时间为10秒,默认压缩方式为无压缩
sofa::pbrpc::RpcC
controller.SetTimeout(3000);
// 发起调用,最后一个参数为NULL表示为同步调用
stub.Echo(&controller, &request, &response, NULL);
// 调用完成后,检查是否失败
if (controller.Failed()) {
// 调用失败后的错误处理,譬如可以进行重试
SLOG(ERROR, "request failed: %s", controller.ErrorText().c_str());
return EXIT_SUCCESS;
sofa-pbRPC的使用并没有像sofa这个名字那样sofa,根据标准1,服务端的RPC接口比gRPC更加复杂,更加远离本地调用了。根据标准2,用户要做很多额外的事,需要关注框架的很多细节,比较难用。根据标准3,同样只支持pb协议,无法支持其他协议。
综合评价:62分。
腾讯Pebble
腾讯开源的Pebble也是基于protocal buffer的,不过他的用法比gRPC和sofaRPC更好用,思路都是类似的,先定义协议。
struct HeartBeatInfo {
1: i64 id,
2: i32 version = 1,
3: string address,
4: optional string comment,
service BaseService {
i64 heartbeat(1:i64 id, 2:HeartBeatInfo data),
oneway void log(1: string content)
服务器端代码
class BaseServiceHandler : public BaseServiceCobSvIf {
void log(const std::string& content) {
std::cout && "receive request : log(" && content && ")" && std::
int main(int argc, char* argv[]) {
// 初始化RPC
pebble::rpc::Rpc* rpc = pebble::rpc::Rpc::Instance();
rpc-&Init("", 0, "");
// 注册服务
BaseServiceHandler base_
rpc-&RegisterService(&base_service);
// 配置服务监听地址
std::string listen_addr("tcp://127.0.0.1:");
if (argc & 1) {
listen_addr.append(argv[1]);
listen_addr.append("8200");
// 添加服务监听地址
rpc-&AddServiceManner(listen_addr, pebble::rpc::PROTOCOL_BINARY);
// 启动server
rpc-&Serve();
客户端代码
// 初始化RPC
pebble::rpc::Rpc* rpc = pebble::rpc::Rpc::Instance();
rpc-&Init("", -1, "");
// 创建rpc client stub
BaseServiceClient client(service_url, pebble::rpc::PROTOCOL_BINARY);
// 同步调用
int ret = client.log("pebble simple test : log");
std::cout && "sync call, ret = " && ret && std::
Pebble比gRPC和sofa-pbrpc更好用,根据标准1,调用方式和本地调用一致了,接口中没有任何限制。根据标准2,除了定义协议稍显繁琐之外已经比较易用了,不过服务器在使用上还是有一些限制,比如注册服务的时候只能注册一个类对象的指针,不能支持lambda表达式,std::function或者普通的function。根据标准3,gRPC只支持pb协议,无法扩展支持其他协议。
综合评价:75分。
apache msgpack-RPC
是基于msgpack定义的RPC框架,不同于基于pb的RPC,他无需定义专门的协议。
服务器端代码
#include &jubatus/msgpack/rpc/server.h&
class myserver : public msgpack::rpc::server::base {
void add(msgpack::rpc::request req, int a1, int a2)
req.result(a1 + a2);
void dispatch(msgpack::rpc::request req)
req.method().convert(&method);
if(method == "add") {
msgpack::type::tuple&int, int&
req.params().convert(&params);
add(req, params.get&0&(), params.get&1&());
req.error(msgpack::rpc::NO_METHOD_ERROR);
} catch (msgpack::type_error& e) {
req.error(msgpack::rpc::ARGUMENT_ERROR);
} catch (std::exception& e) {
req.error(std::string(e.what()));
客户端代码
#include &jubatus/msgpack/rpc/client.h&
#include &iostream&
int main(void)
msgpack::rpc::client c("127.0.0.1", 9090);
int result = c.call("add", 1, 2).get&int&();
std::cout && result && std::
msgpack-RPC使用起来也很简单,不需要定义proto文件,根据标准1,客户端的调用和本地调用一致,不过,服务器的RPC接口有一个msgpack::rpc::request对象,并且也必须派生于base类,使用上有一定的限制。根据标准2,服务器端提供RPC服务的时候需要根据method的名字来dispatch,这种方式不符合开闭原则,使用起来有些不方便。根据标准3,msgpack-rpc只支持msgpack的序列化,不能支持其他的序列化方式。
综合评价:80分。
目前虽然国内外各大公司都推出了自己的RPC框架,但是真正好用易用的RPC框架却是不多的,这里对各个厂商的RPC框架仅从好用的角度做一个评价,一家之言,仅供参考,希望可以为大家做RPC的技术选型的时候提供一些评判依据。
能跨跃防火墙吗?
最好的是满足自己需求的,比如我们是全java技术栈的,所以我们就自己实现了一套。
应该叫c++的rpc
阿里的Dubbo在哪里?
都不好,这样才好。1.支持各种序列化,反序列化2.跟本地class 一样调用。而不是 call('name','param')3.要支持各种协议,TCP,UDP,消息队列
class test extend framework{
public int a,
public string add(a, b){
this.a =a; this.b=b;
return a +b
client = new Client(192.168.0.1)
print client.add(11,11)
print client.a
要能这样访问才是好RPC, 除了实例化设置了IP地址,其他用法跟本地实例化对象 100% 一样。自定义的RPC的Java实现 - jbm - ITeye技术网站
博客分类:
在看hadoop的源代码的时候,看到hadoop实现了一个自定义的RPC,于是有了自己写代码实现RPC的想法。
RPC的全名Remote Process Call,即远程过程调用。使用RPC,可以像使用本地的程序一样使用远程服务器上的程序。下面是一个简单的RPC 调用实例,从中可以看到RPC如何使用以及好处:
public class MainClient {
public static void main(String[] args) {
Echo echo = RPC.getProxy(Echo.class, "127.0.0.1", 20382);
System.out.println(echo.echo("hello,hello"));
public interface Echo {
public String echo(String string);
使用RPC.getProxy生成接口Echo的代理实现类。然后就可以像使用本地的程序一样来调用Echo中的echo方法。
使用RPC的好处是简化了远程服务访问。提高了开发效率。在分发代码时,只需要将接口分发给客户端使用,在客户端看来只有接口,没有具体类实现。这样保证了代码的可扩展性和安全性。
在看了RPCClient如何使用,我们再来定义一个RPC服务器的接口,看看服务器都提供什么操作:
public interface Server {
public void stop();
public void start();
public void register(Class interfaceDefiner,Class impl);
public void call(Invocation invo);
public boolean isRunning();
public int getPort();
服务器提供了start和stop方法。使用register注册一个接口和对应的实现类。call方法用于执行Invocation指定的接口的方法名。isRunning返回了服务器的状态,getPort()则返回了服务器使用的端口。
来看看Invocation的定义:
public class Invocation implements Serializable{
private static final long serialVersionUID = 1L;
private Object[]
* @return the result
public Object getResult() {
* @param result the result to set
public void setResult(Object result) {
this.result =
* @return the interfaces
public Class getInterfaces() {
* @param interfaces the interfaces to set
public void setInterfaces(Class interfaces) {
this.interfaces =
* @return the method
public Method getMethod() {
* @param method the method to set
public void setMethod(Method method) {
this.method =
* @return the params
public Object[] getParams() {
* @param params the params to set
public void setParams(Object[] params) {
this.params =
public String toString() {
return interfaces.getName()+"."+method.getMethodName()+"("+Arrays.toString(params)+")";
具体服务器实现类中的call方法是这样使用Invocation的:
public void call(Invocation invo) {
Object obj = serviceEngine.get(invo.getInterfaces().getName()); //根据接口名,找到对应的处理类
if(obj!=null) {
Method m = obj.getClass().getMethod(invo.getMethod().getMethodName(), invo.getMethod().getParams());
Object result = m.invoke(obj, invo.getParams());
invo.setResult(result);
} catch (Throwable th) {
th.printStackTrace();
throw new IllegalArgumentException("has no these class");
下面来看服务器接收连接并处理连接请求的核心代码:
public class Listener extends Thread {
private ServerS
public Listener(Server server) {
this.server =
public void run() {
System.out.println("启动服务器中,打开端口" + server.getPort());
socket = new ServerSocket(server.getPort());
} catch (IOException e1) {
e1.printStackTrace();
while (server.isRunning()) {
Socket client = socket.accept();
ObjectInputStream ois = new ObjectInputStream(client.getInputStream());
Invocation invo = (Invocation) ois.readObject();
server.call(invo);
ObjectOutputStream oos = new ObjectOutputStream(client.getOutputStream());
oos.writeObject(invo);
oos.flush();
oos.close();
ois.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
if (socket != null && !socket.isClosed())
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
RPC具体的Server类是这样来使用Listener的:
public static class RPCServer implements Server{
private int port = 20382;
private boolean isRuning =
* @param isRuning the isRuning to set
public void setRuning(boolean isRuning) {
this.isRuning = isR
* @return the port
public int getPort() {
* @param port the port to set
public void setPort(int port) {
this.port =
private Map&String ,Object& serviceEngine = new HashMap&String, Object&();
public void call(Invocation invo) {
System.out.println(invo.getClass().getName());
Object obj = serviceEngine.get(invo.getInterfaces().getName());
if(obj!=null) {
Method m = obj.getClass().getMethod(invo.getMethod().getMethodName(), invo.getMethod().getParams());
Object result = m.invoke(obj, invo.getParams());
invo.setResult(result);
} catch (Throwable th) {
th.printStackTrace();
throw new IllegalArgumentException("has no these class");
public void register(Class interfaceDefiner, Class impl) {
this.serviceEngine.put(interfaceDefiner.getName(), impl.newInstance());
System.out.println(serviceEngine);
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
public void start() {
System.out.println("启动服务器");
listener = new Listener(this);
this.isRuning =
listener.start();
public void stop() {
this.setRuning(false);
public boolean isRunning() {
return isR
服务器端代码搞定后,来看看客户端的代码,先看看我们刚开始使用RPC.getProxy方法:
public static &T& T getProxy(final Class&T& clazz,String host,int port) {
final Client client = new Client(host,port);
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invo = new Invocation();
invo.setInterfaces(clazz);
invo.setMethod(new org.jy.rpc.protocal.Method(method.getName(),method.getParameterTypes()));
invo.setParams(args);
client.invoke(invo);
return invo.getResult();
T t = (T) Proxy.newProxyInstance(RPC.class.getClassLoader(), new Class[] {clazz}, handler);
Client类的代码如下:
public class Client {
private ObjectOutputS
private ObjectInputS
public String getHost() {
public void setHost(String host) {
this.host =
public int getPort() {
public void setPort(int port) {
this.port =
public Client(String host, int port) {
this.host =
this.port =
public void init() throws UnknownHostException, IOException {
socket = new Socket(host, port);
oos = new ObjectOutputStream(socket.getOutputStream());
public void invoke(Invocation invo) throws UnknownHostException, IOException, ClassNotFoundException {
System.out.println("写入数据");
oos.writeObject(invo);
oos.flush();
ois = new ObjectInputStream(socket.getInputStream());
Invocation result = (Invocation) ois.readObject();
invo.setResult(result.getResult());
至此,RPC的客户端和服务器端代码完成,启动服务器的代码如下:
public class Main {
public static void main(String[] args) {
Server server = new RPC.RPCServer();
server.register(Echo.class, RemoteEcho.class);
server.start();
现在先运行服务器端代码,再运行客户端代码,就可以成功运行。
详细的代码,参考附件的源代码。
在写这个RPC时,没有想太多。在数据串行化上,使用了java的标准io序列化机制,虽然不能跨平台,但是做DEMO还是不错的;另外在处理客户端请求上,使用了ServerSocket,而没有使用ServerSocketChannel这个java nio中的新特性;在动态生成接口的实现类上,使用了java.lang.reflet中的Proxy类。他可以动态创建接口的实现类。
下载次数: 1275
浏览 22756
[color=cyan][/color][/list][/img][/flash]&&&&&&
浏览: 123721 次
来自: 北京
好东西,谢谢大哥
不能跨平台,和RMI相比有何优势呢?能否提供个跨平台的方案,比 ...
写得很不错 受启发966,690 十一月 独立访问用户
语言 & 开发
架构 & 设计
文化 & 方法
您目前处于:
Netty系列之Netty安全性
Netty系列之Netty安全性
欲知区块链、VR、TensorFlow等潮流技术、框架等,请锁定
相关厂商内容
相关赞助商
更多AWS最新精彩内容和活动,!
任何网络攻击都能够给企业造成破坏,但是如何将这些破坏具体量化成金融数据呢?2013年,B2B International联合卡巴斯基实验室基于对全球企业的调查结果,计算出网络攻击平均造成的损失。
根据调查报告得出的结论,当企业遭遇网络攻击后平均损失为649,000美元。损失主要包括两方面:
安全事件本身造成的损失,即由重要数据泄漏、业务连续性以及安全修复专家费用相关成本;
为列入计划的&响应&成本,用于阻止未来发生类似的攻击事件,包括雇佣、培训员工成本以及硬件、软件和其它基础设施安全升级成本。
1.2. Netty面临的安全风险
作为一个高性能的NIO通信框架,基于Netty的行业应用非常广泛,不同的行业、不同的应用场景,面临的安全挑战也不同,下面我们根据Netty的典型应用场景,分析下Netty面临的安全挑战。
1.2.1. 仅限内部使用的RPC通信框架
随着业务的发展,网站规模的扩大,传统基于MVC的垂直架构已经无法应对业务的快速发展。需要对数据和业务进行水平拆分,基于RPC的分布式服务框架成为最佳选择。
业务水平拆分之后,内部的各个模块需要进行高性能的通信,传统基于RMI和Hession的同步阻塞式通信已经无法满足性能和可靠性要求。因此,高性能的NIO框架成为构建分布式服务框架的基石。
网站的架构演进过程如下:
图1-1 网站的架构演进
高性能的RPC框架,各模块之间往往采用长连接通信,通过心跳检测保证链路的可靠性。由于RPC框架通常是在内部各模块之间使用,运行在授信的内部安全域中,不直接对外开放接口。因此,不需要做握手、黑白名单、SSL/TLS等,正所谓是&防君子不防小人&。
在这种应用场景下,Netty的安全性是依托企业的防火墙、安全加固操作系统等系统级安全来保障的,它自身并不需要再做额外的安全性保护工作。
1.2.2. 对第三方开放的通信框架
如果使用Netty做RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些能力通过服务对外开放出去,此时就需要进行安全认证,如果开放的是公网IP,对于安全性要求非常高的一些服务,例如在线支付、订购等,需要通过SSL/TLS进行通信。
它的原理图如下:
图1-2 对第三方开放的通信框架
对第三方开放的通信框架的接口调用存在三种场景:
在企业内网,开放给内部其它模块调用的服务,通常不需要进行安全认证和SSL/TLS传输;
在企业内网,被外部其它模块调用的服务,往往需要利用IP黑白名单、握手登陆等方式进行安全认证,认证通过之后双方使用普通的Socket进行通信,如果认证失败,则拒绝客户端连接;
开放给企业外部第三方应用访问的服务,往往需要监听公网IP(通常是防火墙的IP地址),由于对第三方服务调用者的监管存在诸多困难,或者无法有效监管,这些第三方应用实际是非授信的。为了有效应对安全风险,对于敏感的服务往往需要通过SSL/TLS进行安全传输。
1.2.3. 应用层协议的安全性
作为高性能、异步事件驱动的NIO框架,Netty非常适合构建上层的应用层协议,相关原理,如下图所示:
图1-3 基于Netty构建应用层协议
由于绝大多数应用层协议都是公有的,这意味着底层的Netty需要向上层提供通信层的安全传输,也就是需要支持SSL/TLS。
JDK的安全类库提供了javax.net.ssl.SSLSocket和javax.net.ssl.SSLServerSocket类库用于支持SSL/TLS安全传输,对于NIO非阻塞Socket通信,JDK并没有提供现成可用的类库简化用户开发。
Netty通过JDK的SSLEngine,以SslHandler的方式提供对SSL/TLS安全传输的支持,极大的简化了用户的开发工作量,降低开发难度。
对于Netty默认提供的HTTP协议,Netty利用SslHandler,同样支持HTTPS协议。
2. Netty SSL开发
2.1. SSL单向认证
单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。下面我们通过Netty的SSL单向认证代码开发来掌握基于Netty的SSL单向认证。
2.1.1. SSL单向认证开发
首先,利用JDK的keytool工具,Netty服务端依次生成服务端的密钥对和证书仓库、服务端自签名证书。
生成Netty服务端私钥和证书仓库命令:
keytool -genkey -alias securechat -keysize 2048 -validity
365 -keyalg RSA -dname &CN=localhost& -keypass sNetty
-storepass sNetty -keystore sChat.jks
生成Netty服务端自签名证书:
keytool -export -alias securechat -keystore sChat.jks -storepass sNetty -file sChat.cer
生成客户端的密钥对和证书仓库,用于将服务端的证书保存到客户端的授信证书仓库中,命令如下:
keytool -genkey -alias smcc -keysize 2048 -validity 365
-keyalg RSA -dname &CN=localhost& -keypass cNetty
-storepass cNetty -keystore cChat.jks
随后,将Netty服务端的证书导入到客户端的证书仓库中,命令如下:
keytool -import -trustcacerts -alias securechat -file sChat.cer -storepass cNetty -keystore cChat.jks
上述工作完成之后,我们就开始编写SSL服务端和客户端的代码,下面我们对核心代码进行讲解。
首先看服务端的代码,在TCP链路初始化的时候,创建SSLContext并对其进行正确的初始化,下面我们对SSLContext的创建进行讲解:
因为是客户端认证服务端,因此服务端需要正确的设置和加载私钥仓库KeyStore,相关代码如下:
初始化KeyManagerFactory之后,创建SSLContext并初始化,代码如下:
由于是单向认证,服务端不需要验证客户端的合法性,因此,TrustManager为空,安全随机数不需要设置,使用JDK默认创建的即可。
服务端的SSLContext创建完成之后,利用SSLContext创建SSL引擎SSLEngine,设置SSLEngine为服务端模式,由于不需要对客户端进行认证,因此NeedClientAuth不需要额外设置,使用默认值False。相关代码如下:
SSL服务端创建完成之后,下面继续看客户端的创建,它的原理同服务端类似,也是在初始化TCP链路的时候创建并设置SSLEngine,代码如下:
由于是客户端认证服务端,因此,客户端只需要加载存放服务端CA的证书仓库即可。
加载证书仓库完成之后,初始化SSLContext,代码如下:对于客户端只需要设置信任证书TrustManager。
客户端SSLContext初始化完成之后,创建SSLEngine并将其设置为客户端工作模式,代码如下:
将SslHandler添加到pipeline中,利用SslHandler实现Socket安全传输,代码如下:
客户端和服务端创建完成之后,测试下SSL单向认证功能是否OK,为了查看SSL握手过程,我们打开SSL握手的调测日志,Eclipse设置如下:
图2-1 打开SSL调测日志
分别运行服务端和客户端,运行结果如下:
图2-2 客户端SSL握手日志
图2-3 服务端SSL握手日志
在客户端输入信息,服务端原样返回,测试结果如下:
到此,Netty SSL单向认证已经开发完成,下个小节我们将结合SSL握手日志,详细解读下SSL单向认证的原理。
2.1.2. SSL单向认证原理分析
SSL单向认证的过程总结如下:
SSL客户端向服务端传送客户端SSL协议的版本号、支持的加密算法种类、产生的随机数,以及其它可选信息;
服务端返回握手应答,向客户端传送确认SSL协议的版本号、加密算法的种类、随机数以及其它相关信息;
服务端向客户端发送自己的公钥;
客户端对服务端的证书进行认证,服务端的合法性校验包括:证书是否过期、发行服务器证书的CA是否可靠、发行者证书的公钥能否正确解开服务器证书的&发行者的数字签名&、服务器证书上的域名是否和服务器的实际域名相匹配等;
客户端随机产生一个用于后面通讯的&对称密码&,然后用服务端的公钥对其加密,将加密后的&预主密码&传给服务端;
服务端将用自己的私钥解开加密的&预主密码&,然后执行一系列步骤来产生主密码;
客户端向服务端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知服务器客户端的握手过程结束;
服务端向客户端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知客户端服务器端的握手过程结束;
SSL的握手部分结束,SSL安全通道建立,客户端和服务端开始使用相同的对称密钥对数据进行加密,然后通过Socket进行传输;
下面,我们结合JDK的SSL工作原理对Netty的SSL单向认证过程进行讲解,首先,我们看下JDK SSL单向认证的流程图:
图2-4 SSL单向认证流程图
下面结合JDK SSL引擎的调测日志信息我们对SSL单向认证的流程进行详细讲解,对于比较简单的流程会进行步骤合并。
步骤1:客户端使用TLS协议版本发送一个ClientHello消息,这个消息包含一个随机数、建议的加密算法套件和压缩方法列表,如下所示:
*** ClientHello, TLSv1
RandomCookie:
bytes = { 125, 107, 138, 150, 226, 182, 238, 75, 38,
150, 222, 147, 127, 35, 36, 149, 172, 128, 152, 34, 110, 104, 176, 34, 180, 118, 185, 55 }
Session ID:
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
TLS_ECDHE_RSA_WITH_RC4_128_SHA, SSL_RSA_WITH_RC4_128_SHA, TLS_ECDH_ECDSA_WITH_RC4_128_SHA,
TLS_ECDH_RSA_WITH_RC4_128_SHA, TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
SSL_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_RC4_128_MD5,
TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods:
Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1,
sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, sect409r1, secp521r1, sect571k1,
sect571r1, secp160k1, secp160r1, secp160r2, sect163r1, secp192k1, sect193r1, sect193r2, secp224k1,
sect239k1, secp256k1}
Extension ec_point_formats, formats: [uncompressed]
步骤2:服务端使用ServerHello消息来响应,这个消息包含由客户提供的信息基础上的另一个随机数和一个可选的会话ID,以及服务端选择的加密套件算法,响应消息如下:
*** ServerHello, TLSv1
RandomCookie:
bytes = { 27, 170, 76, 238, 56, 58, 172, 146,
41, 159, 249, 213, 16, 214, 53, 167, 50, 74, 39, 107, 121, 63, 80, 26, 210, 149, 249, 194 }
Session ID:
{83, 215, 155, 12, 122, 5, 231, 3, 13, 11, 17, 204, 56, 73, 119,
49, 85, 229, 220, 92, 55, 40, 25, 194, 198, 244, 200, 6, 55, 209, 23, 245}
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: &empty&
步骤3:服务端发送自签名的证书消息,包含完整的证书链:
*** Certificate chain
chain [0] = [
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840..1.5
Sun RSA public key, 2048 bits
modulus: 299212
public exponent: 65537
Validity: [From: Sun Jul 27 08:49:30 CST 2014,
To: Mon Jul 27 08:49:30 CST 2015]
Issuer: CN=localhost
SerialNumber: [
Algorithm: [SHA1withRSA]
Signature:
5E D4 EE A8 1C 8E
82 F1 3F 6B 0A 34 9B 96
..^.......?k.4..
0010: 97 BE 62 13 F7 2E 94 74
A5 46 CC AB C5 0B FC 67
..b....t.F.....g
0020: 3C E1 1B 43 B8 A4 3B C9
F9 44 9F F2 D2 90 35 3C
&..C..;..D....5&
0030: F6 47 78 3A AC 6B 87 E5
43 EA C8 C5 8C 4C 6E AB
.Gx:.k..C....Ln.
C8 C4 BA 86 97 1E
C5 75 2F 85 15 CB A1 93
F........u/.....
06 57 93 47 DF 8D
04 0F 21 AC FC E0 7D 14
.#.W.G....!.....
0060: 07 BE 0F 62 F4 75 A9 CE
F9 B3 11 0B 75 B4 87 22
...b.u......u..&
0070: D5 8E E2 0A A9 1F C2 15
3A 64 B2 23 8F 1A 84 6C
........:d.#...l
0080: EE 2C 3A C3 24 65 F5 BC
5C AF BD F8 B9 C4 45 83
.,:.$e..\.....E.
0090: 5B FF BD 36 E8 5D BE 98
03 2E AB 3F FE EC 9A 7B
[..6.].....?....
00A0: 31 35 7D EF 53 81 8B 7A
8B 37 7D BD EB 17 F0 36
15..S..z.7.....6
00B0: 93 CF 74 28 A3 C1 8B E1
B1 12 9F 44 20 CA 48 64
..t(.......D .Hd
00C0: D6 F5 B0 B1 D9 18 AA F6
88 02 26 93 C8 B8 91 1A
..........&.....
00D0: F8 B0 8B E6 7D C6 56 39
B2 6A AF 73 D2 78 76 1A
......V9.j.s.xv.
00E0: 10 F0 C5 98 4F 90 39 2F
84 BC A0 78 81 8B ED 04
....O.9/...x....
00F0: B8 60 49 84 C3 BD CC D2
CA 52 0A 03 E0 6C 21 B3
.`I......R...l!.
步骤4:服务端向客户端发送自己的公钥信息,最后发送ServerHelloDone:
*** ECDH ServerKeyExchange
Server key: Sun EC public key, 256 bits
public x coord: 70697
public y coord: 53157
parameters: secp256r1 [NIST P-256, X9.62 prime256v1] (1.2.840..7)
*** ServerHelloDone
步骤5:客户端对服务端自签名的证书进行认证,如果客户端的信任证书列表中包含了服务端发送的证书,对证书进行合法性认证,相关信息如下:
Found trusted certificate:
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840..1.5
Sun RSA public key, 2048 bits
modulus: 29921
public exponent: 65537
Validity: [From: Sun Jul 27 08:49:30 CST 2014,
To: Mon Jul 27 08:49:30 CST 2015]
Issuer: CN=localhost
SerialNumber: [
Algorithm: [SHA1withRSA]
Signature:
5E D4 EE A8 1C 8E
82 F1 3F 6B 0A 34 9B 96
..^.......?k.4..
0010: 97 BE 62 13 F7 2E 94 74
A5 46 CC AB C5 0B FC 67
..b....t.F.....g
0020: 3C E1 1B 43 B8 A4 3B C9
F9 44 9F F2 D2 90 35 3C
&..C..;..D....5&
0030: F6 47 78 3A AC 6B 87 E5
43 EA C8 C5 8C 4C 6E AB
.Gx:.k..C....Ln.
C8 C4 BA 86 97 1E
C5 75 2F 85 15 CB A1 93
F........u/.....
06 57 93 47 DF 8D
04 0F 21 AC FC E0 7D 14
.#.W.G....!.....
0060: 07 BE 0F 62 F4 75 A9 CE
F9 B3 11 0B 75 B4 87 22
...b.u......u..&
0070: D5 8E E2 0A A9 1F C2 15
3A 64 B2 23 8F 1A 84 6C
........:d.#...l
0080: EE 2C 3A C3 24 65 F5 BC
5C AF BD F8 B9 C4 45 83
.,:.$e..\.....E.
0090: 5B FF BD 36 E8 5D BE 98
03 2E AB 3F FE EC 9A 7B
[..6.].....?....
00A0: 31 35 7D EF 53 81 8B 7A
8B 37 7D BD EB 17 F0 36
15..S..z.7.....6
00B0: 93 CF 74 28 A3 C1 8B E1
B1 12 9F 44 20 CA 48 64
..t(.......D .Hd
00C0: D6 F5 B0 B1 D9 18 AA F6
88 02 26 93 C8 B8 91 1A
..........&.....
00D0: F8 B0 8B E6 7D C6 56 39
B2 6A AF 73 D2 78 76 1A
......V9.j.s.xv.
00E0: 10 F0 C5 98 4F 90 39 2F
84 BC A0 78 81 8B ED 04
....O.9/...x....
00F0: B8 60 49 84 C3 BD CC D2
CA 52 0A 03 E0 6C 21 B3
.`I......R...l!.
步骤6:客户端通知服务器改变加密算法,通过Change Cipher Spec消息发给服务端,随后发送Finished消息,告知服务器请检查加密算法的变更请求:
nioEventLoopGroup-2-1, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
步骤7:服务端读取到Change Cipher Spec变更请求消息,向客户端返回确认密钥变更消息,最后通过发送Finished消息表示SSL/TLS握手结束。
nioEventLoopGroup-3-1, READ: TLSv1 Change Cipher Spec, length = 1
nioEventLoopGroup-3-1, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data:
{ 157, 255, 187, 52, 139, 16, 20, 190, 11, 35, 79, 0 }
nioEventLoopGroup-3-1, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
2.2. SSL双向认证
2.2.1. SSL双向认证开发
我们在2.1章节的基础上进行开发,与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。
首先,生成客户端的自签名证书:
keytool -export -alias smcc -keystore cChat.jks -storepass cNetty
-file cChat.cer
最后,将客户端的自签名证书导入到服务端的信任证书仓库中:
keytool -import -trustcacerts -alias smcc -file cChat.cer -storepass
sNetty -keystore sChat.jks
证书导入之后,需要对SSL客户端和服务端的代码同时进行修改,首先我们看下服务端如何修改。
由于服务端需要对客户端进行验证,因此在初始化服务端SSLContext的时候需要加载证书仓库。首先需要对TrustManagerFactory进行初始化,代码如下:
初始化SSLContext的时候根据TrustManagerFactory获取TrustManager数组,代码如下:
最后,创建SSLEngine之后,设置需要进行客户端认证,代码如下:
完成服务端修改之后,再回头看下客户端的修改,由于服务端需要认证客户端的证书,因此,需要初始化和加载私钥仓库,向服务端发送公钥,初始化KeyStore的代码如下:
初始化SSLContext的时候需要传入KeyManager数组,代码如下:
客户端开发完成之后,测试下程序是否能够正常工作,运行结果如下所示。
客户端运行结果:
图2-5 Netty SSL双向认证客户端运行结果
服务端运行结果:
图2-6 Netty SSL双向认证服务端运行结果
在客户端控制台进行输入,看SSL传输是否正常:
图2-7 Netty SSL 安全传输测试
2.2.2. SSL双向认证原理分析
SSL双向认证相比单向认证,多了一步服务端发送认证请求消息给客户端,客户端发送自签名证书给服务端进行安全认证的过程。下面,我们结合Netty SSL调测日志,对双向认证的差异点进行分析。
相比于客户端,服务端在发送ServerHello时携带了要求客户端认证的请求信息,如下所示:
*** CertificateRequest
Cert Types: RSA, DSS, ECDSA
Cert Authorities:
&CN=localhost&
&CN=localhost&
*** ServerHelloDone
客户端接收到服务端要求客户端认证的请求消息之后,发送自己的证书信息给服务端,信息如下:
matching alias: smcc
*** Certificate chain
chain [0] = [
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840..1.5
Sun RSA public key, 2048 bits
modulus: 074
public exponent: 65537
Validity: [From: Sun Jul 27 08:50:35 CST 2014,
To: Mon Jul 27 08:50:35 CST 2015]
Issuer: CN=localhost
SerialNumber: [
服务端对客户端的自签名证书进行认证,信息如下:
Found trusted certificate:
Version: V3
Subject: CN=localhost
Signature Algorithm: SHA1withRSA, OID = 1.2.840..1.5
Sun RSA public key, 2048 bits
modulus: 07
public exponent: 65537
Validity: [From: Sun Jul 27 08:50:35 CST 2014,
To: Mon Jul 27 08:50:35 CST 2015]
Issuer: CN=localhost
SerialNumber: [
2.3. 第三方CA认证
使用jdk keytool生成的数字证书是自签名的。自签名就是指证书只能保证自己是完整且没有经过非法修改,但是无法保证这个证书是属于谁的。为了对自签名证书进行认证,需要每个客户端和服务端都交换自己自签名的私有证书,对于一个大型网站或者应用服务器,这种工作量是非常大的。
基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些CA_ROOT签名过的。就可以通过验证了。
CA数字证书认证服务往往是收费的,国内有很多数字认证中心都提供相关的服务,如下所示:
图2-8 商业的数字认证中心
作为示例,我们自己生成一个CA_ROOT的密钥对,部署应用时,把这个CA_ROOT的私钥部署在所有需要SSL传输的节点就可以完成安全认证。作为示例,如果要生成CA_ROOT,我们使用开源的OpenSSL。
在Windows上安装和使用OpenSSL网上有很多教程,也不是本文的重点,因此,OpenSSL的安装和使用本文不详细介绍。
下面我们对基于第三方CA认证的步骤进行详细介绍。
2.3.1. 服务端证书制作
步骤1:利用OpenSSL生成CA证书:
openssl req -new -x509 -keyout ca.key -out ca.crt -days 365
步骤2:生成服务端密钥对:
keytool -genkey -alias securechat -keysize 2048 -validity 365
-keyalg RSA -dname &CN=localhost& -keypass sNetty -storepass sNetty
-keystore sChat.jks
步骤3:生成证书签名请求:
keytool -certreq -alias securechat -sigalg MD5withRSA -file
-keypass sNetty -storepass sNetty -keystore sChat.jks
步骤4:用CA私钥进行签名:
openssl ca -in sChat.csr -out sChat.crt -cert ca.crt -keyfile ca.key -notext
步骤5:导入信任的CA根证书到keystore:
keytool -import -v -trustcacerts -alias ca_root -file ca.crt -storepass
sNetty -keystore sChat.jks
步骤6:将CA签名后的server端证书导入keystore:
keytool -import -v -alias securechat -file server.crt -keypass sNetty
-storepass sNetty -keystore sChat.jks
2.3.2. 客户端证书制作
步骤1:生成客户端密钥对:
keytool -genkey -alias smcc -keysize 2048 -validity 365 -keyalg
RSA -dname &CN=localhost& -keypass cNetty -storepass cNetty -keystore cChat.jks
步骤2:生成证书签名请求:
keytool -certreq -alias smcc -sigalg MD5withRSA -file
-keypass cNetty -storepass cNetty -keystore cChat.jks
步骤3:用CA私钥进行签名:
openssl ca -in cChat.csr -out cNetty.crt -cert ca.crt -keyfile ca.key -notext
步骤4:导入信任的CA根证书到keystore:
keytool -import -v -trustcacerts -alias ca_root -file ca.crt
-storepass cNetty -keystore cChat.jks
步骤5:将CA签名后的client端证书导入keystore:
keytool -import -v -alias smcc -file cNetty.crt -keypass cNetty -storepass
cNetty -keystore cChat.jks
2.3.3. 开发和测试
基于CA认证的开发和测试与SSL双向和单向认证代码相同,此处不再赘述。
3. Netty SSL源码分析
3.1. SSL客户端
当客户端和服务端的TCP链路建立成功之后,SslHandler的channelActive被触发,SSL客户端通过SSL引擎发起握手请求消息,代码如下:
发起握手请求之后,需要将SSLEngine创建的握手请求消息进行SSL编码,发送给服务端,因此,握手之后立即调用wrapNonAppData方法,下面具体对该方法进行分析:
因为只需要发送握手请求消息,因此Source ByteBuf为空,下面看下wrap方法的具体实现:
将SSL引擎中创建的握手请求消息编码到目标ByteBuffer中,然后对写索引进行更新。判断写入操作是否越界,如果越界说明out容量不足,需要调用ensureWritable对ByteBuf进行动态扩展,扩展之后继续尝试编码操作。如果编码成功,返回SSL引擎操作结果。
对编码结果进行判断,如果编码字节数大于0,则将编码后的结果发送给服务端,然后释放临时变量out。
判断SSL引擎的操作结果,SSL引擎的操作结果定义如下:
FINISHED:SSLEngine 已经完成握手;
NEED_TASK:SSLEngine 在继续进行握手前需要一个(或多个)代理任务的结果;
NEED_UNWRAP:在继续进行握手前,SSLEngine 需要从远端接收数据,所以应带调用SSLEngine.unwrap();
NEED_WRAP:在继续进行握手前,SSLEngine 必须向远端发送数据,所以应该调用 SSLEngine.wrap();
NOT_HANDSHAKING:SSLEngine 当前没有进行握手。
下面我们分别对5种操作的代码进行分析:
如果握手成功,则设置handshakePromise的操作结果为成功,同时发送SslHandshakeCompletionEvent.SUCCES给SSL监听器,代码如下:
如果是NEED_TASK,说明异步执行SSL Task,完成后续可能耗时的操作或者任务,Netty封装了一个任务立即执行线程池专门处理SSL的代理任务,代码如下:
如果是NEED_UNWRAP,则判断是否由UNWRAP发起,如果不是则执行UNWRAP操作。
如果是NOT_HANDSHAKING,则调用unwrap,继续接收服务端的消息。
服务端应答消息的接收跟服务端接收客户端的代码类似,唯一不同之处在于SSL引擎的客户端模式设置不同,一个是服务端,一个是客户端。上层的代码处理是相同的,下面我们在SSL服务端章节分析握手消息的接收。
3.2. SSL服务端
SSL服务端接收客户端握手请求消息的入口方法是decode方法,下面对它进行详细分析。
首先获取接收缓冲区的读写索引,并对读取的偏移量指针进行备份:
对半包标识进行判断,如果上一个消息是半包消息,则判断当前可读的字节数是否小于整包消息的长度,如果小于整包长度,则说明本次读取操作仍然没有把SSL整包消息读取完整,需要返回IO线程继续读取,代码如下:
如果消息读取完整,则修改偏移量:同时置位半包长度标识。
下面在for循环中读取SSL消息,因为TCP存在拆包和粘包,因此一个ByteBuf可能包含多条完整的SSL消息。
首先判断可读的字节数是否小于协议消息头长度,如果是则退出循环继续由IO线程接收后续的报文:
获取SSL消息包的报文长度,具体算法不再介绍,可以参考SSL的规范文档进行解读,代码如下:
对长度进行判断,如果SSL报文长度大于可读的字节数,说明是个半包消息,将半包标识长度置位,返回IO线程继续读取后续的数据报,代码如下:
对消息进行解码,将SSL加密的消息解码为加密前的原始数据,unwrap方法如下:
调用SSLEngine的unwrap方法对SSL原始消息进行解码,对解码结果进行判断,如果越界,说明out缓冲区不够,需要进行动态扩展。如果是首次越界,为了尽量节约内存,使用SSL最大缓冲区长度和SSL原始缓冲区可读的字节数中较小的。如果再次发生缓冲区越界,说明扩张后的缓冲区仍然不够用,直接使用SSL缓冲区的最大长度,保证下次解码成功。
解码成功之后,对SSL引擎的操作结果进行判断:如果需要继续接收数据,则继续执行解码操作;如果需要发送握手消息,则调用wrapNonAppData发送握手消息;如果需要异步执行SSL代理任务,则调用立即执行线程池执行代理任务;如果是握手成功,则设置SSL操作结果,发送SSL握手成功事件;如果是
应用层的业务数据,则继续执行解码操作,其它操作结果,抛出操作类型异常。
需要指出的是,SSL客户端和服务端接收对方SSL握手消息的代码是相同的,那为什么SSL服务端和客户端发送的握手消息不同呢?这些是SSL引擎负责区分和处理的,我们在创建SSL引擎的时候设置了客户端模式,SSL引擎就是根据这个来进行区分的,代码如下:
无论客户端还是服务端,只需要围绕SSL引擎的操作结果进行编程即可。
3.3. SSL消息读取
SSL的消息读取实际就是ByteToMessageDecoder将接收到的SSL加密后的报文解码为原始报文,然后将整包消息投递给后续的消息解码器,对消息做二次解码。基于SSL的消息解码模型如下:
SSL消息读取的入口都是decode,因为是非握手消息,它的处理非常简单,就是循环调用引擎的unwrap方法,将SSL报文解码为原始的报文,代码如下:
握手成功之后的所有消息都是应用数据,因此它的操作结果为NOT_HANDSHAKING,遇到此标识之后继续读取消息,直到没有可读的字节,退出循环,代码如下:
如果读取到了可用的字节,则将读取到的缓冲区加到输出结果列表中,代码如下:
ByteToMessageDecoder判断解码结果List,如果非空,则循环调用后续的Handler,由后续的解码器对解密后的报文进行二次解码。
3.4. SSL消息发送
SSL消息发送时,由SslHandler对消息进行编码,编码后的消息实际就是SSL加密后的消息,它的入口是flush方法,代码如下:
从待加密的消息队列中弹出消息,调用SSL引擎的wrap方法进行编码,代码如下:
wrap方法很简单,就是调用SSL引擎的编码方法,然后对写索引进行修改,如果缓冲区越界,则动态扩展缓冲区:
对SSL操作结果进行判断,因为已经握手成功,因此返回的结果是NOT_HANDSHAKING,执行finishWrap方法,调用ChannelHandlerContext的write方法,将消息写入发送缓冲区中,如果待发送的消息为空,则构造空的ByteBuf写入:
编码后,调用ChannelHandlerContext的flush方法消息发送给对方,代码如下:
ctx.flush();
4. Netty学习推荐书籍
目前市面上介绍netty的文章很多,如果读者希望系统性的学习Netty,推荐两本书:
1) 《Netty in Action》,建议阅读英文原版。
2) 《Netty权威指南》,建议通过理论联系实际方式学习。
5. 作者简介
李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通信软件的设计和开发工作,有6年NIO设计和开发经验,精通Netty、Mina等NIO框架,Netty中国社区创始人和Netty框架推广者。
联系方式:新浪微博 Nettying 微信:Nettying Netty学习群:
感谢对本文的策划和审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至。也欢迎大家通过新浪微博()或者腾讯微博()关注我们,并与我们的编辑和其他读者朋友交流。
Author Contacted
告诉我们您的想法
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
与时俱进的干货啊
Re: 与时俱进的干货啊
Re: 你是做技术的吗?这么好的文笔可以去写小说了,O(∩_∩)O!
Re: 高大上
Re: 你是做技术的吗?这么好的文笔可以去写小说了,O(∩_∩)O!
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
赞助商链接
InfoQ每周精要
通过个性化定制的新闻邮件、RSS Feeds和InfoQ业界邮件通知,保持您对感兴趣的社区内容的时刻关注。
架构 & 设计
文化 & 方法
<及所有内容,版权所有 &#169;
C4Media Inc.
服务器由 提供, 我们最信赖的ISP伙伴。
北京创新网媒广告有限公司
京ICP备号-7
注意:如果要修改您的邮箱,我们将会发送确认邮件到您原来的邮箱。
使用现有的公司名称
修改公司名称为:
公司性质:
使用现有的公司性质
修改公司性质为:
使用现有的公司规模
修改公司规模为:
使用现在的国家
使用现在的省份
Subscribe to our newsletter?
Subscribe to our industry email notices?
我们发现您在使用ad blocker。
我们理解您使用ad blocker的初衷,但为了保证InfoQ能够继续以免费方式为您服务,我们需要您的支持。InfoQ绝不会在未经您许可的情况下将您的数据提供给第三方。我们仅将其用于向读者发送相关广告内容。请您将InfoQ添加至白名单,感谢您的理解与支持。

我要回帖

更多关于 dns出现问题怎么解决 的文章

 

随机推荐