一、开发环境
1.Protocol版本:3.9(可用最新)下载地址
2.Protocol协议版本:3
3.普通java环境即可,源码就2个类。
这里主要是使用Java原生的Socket API实先前后端的即时通讯。然后通过ProtocolBuf产生二进制数据(Bytes
)进行网络交互,代码简单易懂,适合对即时通讯的入门。真正项目会采用nio之类的成熟框架来实先这个通讯底层,但是底层原理是相同的。(比如使用Netty)
二、protocol协议文件
前端的请求协议文件
syntax = "proto3";
option java_package = "com.asframe.pb3demo.proto";
option java_outer_classname = "LoginRequest";
message LoginReq
{
//登录类型
int32 type = 1;
//名字
string name = 2;
//密码
string pass = 3;
}
服务端的返回协议文件
syntax = "proto3";
option java_package = "com.asframe.pb3demo.proto";
option java_outer_classname = "LoginResponse";
message LoginRep
{
//登录结果
int32 result = 1;
string msg = 2;
}
注意,其实Protocolbuf的协议结构体没有一定分前后端。这里也只是根据命名来做前后协议的区别,这样好管理。你要合成一个文件也没问题。
执行生成对应的结构体指令:
protoc -I=proto --java_out=src login_rep_msg.proto login_req_msg.proto
根据协议文件生成对应的java代码
三、代码
代码比较简单,注释也比较清晰,就直接贴代码了。
心急的同学也可以先直接下载源码
前端代码:
/**
* 客户端连接测试例子
* @author sodaChen
* @date 2019.07.06
*/
public class Client {
public static void main(String[] args) throws Exception {
//前端socket链接
Socket socket = new Socket("localhost", 19000);
// 读取服务器端传过来信息的DataInputStream
DataInputStream in = new DataInputStream(socket.getInputStream());
// 向服务器端发送信息的DataOutputStream
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
//构造一个LoginReq对象
LoginRequest.LoginReq loginReq = LoginRequest.LoginReq.newBuilder()
.setType(1)
.setName("soda")
.setPass("123456")
.build();
//转成字节,计算发送的pb协议大小
byte[] bodyBytes = loginReq.toByteArray();
//定义消息体头(消息体内容+上版本号的大小)
out.writeShort(bodyBytes.length + 4);
//写版本号
out.writeShort(1);
//消息指令,消息指令是唯一的,返回指令比发送指令多10000,也就是20001
out.writeShort(10001);
//写消息体
out.write(bodyBytes);
out.flush();
//这里接受服务器数据.解析服务器的发送协议流程,基本上客户端发送给服务端类似
//先收消息长度大小
short msgLength = in.readShort();
//消息指令
short cmd = in.readShort();
//剩下的字节都是消息体
byte[] bytes = new byte[msgLength - 2];
in.read(bytes);
//这里规定服务器返回是多1w的协议号。实际项目这里应该做好架构,动态处理
if(cmd == 20001)
{
LoginResponse.LoginRep loginRep = LoginResponse.LoginRep.parseFrom(bytes);
System.out.println("服务器返回数据:" + loginRep);
}
//闭流
out.close();
System.out.println("client close");
}
}
服务端代码的解析过程,其实就是按照前端发送的格式来解析。这是所有通讯的基础。具体的规则其实是可以根据项目的需求进行制定的。
/**
* protocolbuf3的服务端处理
* @author sodaChen
* @date 2019.07.06
*/
public class Server
{
public static void main(String[] args) throws Exception
{
//建立Socket服务器
ServerSocket serverSocket = new ServerSocket(19000);
System.out.println("服务器socket启动.");
while (true)
{
//监听客户端的连接
Socket clientSocket = serverSocket.accept();
System.out.println("有一个客户端连接上来");
// 读取客户端传过来信息的DataInputStream
DataInputStream in = new DataInputStream(clientSocket.getInputStream());
// 向客户端发送信息的DataOutputStream
DataOutputStream out = new DataOutputStream(clientSocket.getOutputStream());
//先收消息长度大小
short msgLength = in.readShort();
//定义消息的版本,目前用不上
short version = in.readShort();
//消息指令,这个指令很重要,用来识别后面的消息体是什么东西来的
short cmd = in.readShort();
//剩下的字节都是消息体(减去已经度的4个字节)
byte[] bytes = new byte[msgLength - 4];
//直接读
in.read(bytes);
//这里直接判断cmd的值,实际做项目的话,这里的代码会进行底层封装,应该是自动解析,而不是手动判断
if(cmd == 10001)
{
//转换成登录请求对象
LoginRequest.LoginReq loginReq = LoginRequest.LoginReq.parseFrom(bytes);
System.out.printf("loginReq:" + loginReq);
//
LoginResponse.LoginRep.Builder builder = LoginResponse.LoginRep.newBuilder();
if(loginReq.getName().equals("soda"))
{
builder.setResult(1);
builder.setMsg("登录成功");
}
else
{
builder.setResult(0);
builder.setMsg("登录失败");
}
//创建登录返回对象
LoginResponse.LoginRep loginRep = builder.build();
//封装消息体发送给前端
//计算发送的pb协议大小
byte[] bodyBytes = loginRep.toByteArray();
//定义消息体头(消息体内容+cmd的大小),服务器不需要返回版本号
out.writeShort(bodyBytes.length + 2);
//消息指令,服务器返回20001
out.writeShort(20001);
//写消息体
out.write(bodyBytes);
out.flush();
}
}
}
}