NIO和Netty全文
C/S长连接
- http协议是无状态的,浏览器和服务器的请求响应一次,下一次会重新连接
WebSocketServerHandler通过状态码101将http协议升级为ws协议,服务器应客户端升级协议的请求对协议进行切换,请求的uri要统一- 第一次发送http请求
- 升级为ws协议
- 服务器回复的
Sec-WebSocket-Accept是对客户端升级协议请求的Sec-WebSocket-Key的验证
server
public class WebSocketServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
new ServerBootstrap().group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO)) //日志处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//基于http协议,使用http编解码器
pipeline.addLast(new HttpServerCodec());
//是以块的方式写,添加chunkedWriteHandler
pipeline.addLast(new ChunkedWriteHandler());
//http数据在传输过程中是分段的,HttpObjectAggregator将多个段聚合
//当浏览器发送大量数据时,发出多次http请求
pipeline.addLast(new HttpObjectAggregator(8192));
//对于webSocket,数据以帧(frame)形式传递,webSocketFrame有六个子类
//识别浏览器访问的资源,浏览器发送请求时 ws://localhost:8888/hello 请求uri
//核心功能将http协议升级为ws协议,保持长连接
pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
//处理业务逻辑
pipeline.addLast(new WebSocketServerHandler());
}
}).bind(8888).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
serverHandler
//TextWebSocketFrame 表示一个文本帧
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
System.out.println("server receive: " + msg.text());
ctx.channel().writeAndFlush(new TextWebSocketFrame("server time: " + LocalDateTime.now() + " - " + msg.text()));
}
//当web客户端连接后触发
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
//id 表示唯一的值 asLongText唯一的 asShortText不唯一
//handlerAdded - acde48fffe001122-00017c52-00000002-2c64cd170f619363-43160a39
System.out.println("handlerAdded - " + ctx.channel().id().asLongText());
//handlerAdded - 43160a39
System.out.println("handlerAdded - " + ctx.channel().id().asShortText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("handlerRemoved - " + ctx.channel().id().asLongText());
System.out.println("handlerRemoved - " + ctx.channel().id().asShortText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
html
<body>
<script>
var socket;
//判断浏览器是否支持websocket
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8888/hello");
//相当于channelRead0 ev收到服务器端回送的消息
socket.onmessage = function (ev) {
var rt = document.getElementById("responseText");
//拼接消息
rt.value = rt.value + "\n" + ev.data;
}
//相当于连接开启
socket.onopen = function (ev) {
var rt = document.getElementById("responseText");
rt.value = "start..."
}
//关闭
socket.onclose = function (ev) {
var rt = document.getElementById("responseText");
rt.value = rt.value + "\n" + "close..."
}
} else {
alert("no support websocket!")
}
function send(message) {
if (!window.socket) { //是否创建好
return;
}
if (socket.readyState == WebSocket.OPEN) {
//发送消息
socket.send(message);
} else {
alert("no connect...")
}
}
</script>
<!--禁止按enter让表单提交-->
<form onsubmit="return false">
<textarea name="message" style="height: 300px; width: 300px"></textarea>
<input type="button" value="send msg" onclick="send(this.form.message.value)">
<textarea id="responseText" style="height: 300px; width: 300px"></textarea>
<input type="button" value="clear" onclick="document.getElementById('responseText').value = ''">
</form>
</body>
protobuf编解码器
- 数据在网络中传输的都是二进制字节码数据,发送时需要编码,接收时需要解码
- 编解码器
codec,包含解码器decoder、编码器encoder - netty自带的编解码器,底层使用的是
java序列化技术,效率不高,无法跨语言,服务器端和客户端需要同一套语言;序列化体积较大,是二进制编码的5倍多
StringEncoder/StringDecoder针对字符串ObjectEncoder/ObjectDecoder针对java对象
Goole Protobuf
- 数据存储,结构化数据,适合
RPC 远程过程调用 remote procedure call数据交换格式,支持跨语言
http+json->tcp+protobuf
- 下载地址
- protobuf文章
- mac安装protobuf
- 使用流程
- 依赖
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.19.4</version>
</dependency>
- 将
.proto文件转换为.java文件,控制台需要跳转到.proto文件目录下
protoc --java_out=. Student.proto
发送Student对象
proto文件
- 生成的
.java文件放入项目中使用
syntax = "proto3"; //协议版本
option java_outer_classname = "StudentPOJO"; //外部类名,同时也是文件名
//以message管理数据
message Student { //会在StudentPOJO 外部类生成一个内部类 Student,真正发送的POJO对象
int32 id = 1; //Student类中有一个属性,名字为id,类型为int32(proto中的类型),对应java中的int,1表示属性序号
string name = 2;
}
服务端添加handler
ch.pipeline().addLast(new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
ch.pipeline().addLast(new SimpleServerHandler());
客户端添加handler
//在pipeline中加入编码器
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new ClientHandler()); //加入自定义处理器
ChannelInboundHandlerAdapter
Handler继承ChannelInboundHandlerAdapter- 客户端
//当通道就绪时触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发送Student对象到服务器
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("xiaoming").build();
ctx.writeAndFlush(student);
}
- 服务端
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("go on...");
//读取从客户端发送的Student
StudentPOJO.Student student = (StudentPOJO.Student) msg;
System.out.println("client: id - " + student.getId() + " name - " + student.getName());
}
SimpleHandler
public class SimpleServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception {
System.out.println("client: id - " + msg.getId() + " name - " + msg.getName());
}
}
发送多个对象
proto文件
syntax = "proto3";
option optimize_for = SPEED; //加快解析
option java_package="com.java.netty.codec2"; //指定生成到哪个包
option java_outer_classname="MyDataInfo"; //外部类名称
//protobuf 可以使用meeage 管理其他的message
message MyMessage {
//定义枚举类型
enum DataType {
StudentType = 0; //要求enum的编号从0开始
WorkerType = 1;
}
//用data_type标识传的枚举类型
//1表示第一个属性
DataType data_type = 1;
//表示下面的属性最多只能出现其中的一个,也就是每个枚举类型只能出现一个
oneof dataBody {
Student student = 2;
Worker worker = 3;
}
}
message Student {
int32 id = 1;
string name = 2;
}
message Worker{
string name = 1;
int32 age = 2;
}
clientHandler
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机发送student和worker
int random = new Random().nextInt(2);
MyDataInfo.MyMessage message = null;
if (0 == random) {
message = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType)
.setStudent(MyDataInfo.Student.newBuilder().setId(1).setName("xiaoming").build()).build();
} else {
message = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType)
.setWorker(MyDataInfo.Worker.newBuilder().setAge(60).setName("laoming").build()).build();
}
ctx.writeAndFlush(message);
}
server
- 服务端配置
//加入解码器,指定对哪种对象进行解码
ch.pipeline().addLast(new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
ch.pipeline().addLast(new SimpleServerHandler());
- SimpleServerHandler
public class SimpleServerHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {
//根据dataType显示不同信息
MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
if (dataType == MyDataInfo.MyMessage.DataType.StudentType) {
System.out.println("student id: " + msg.getStudent().getId() + " name: " + msg.getStudent().getName());
} else if (dataType == MyDataInfo.MyMessage.DataType.WorkerType) {
System.out.println("worker age: " + msg.getWorker().getAge() + " name: " + msg.getWorker().getName());
} else {
System.out.println("no found...");
}
}
}