之前我们实现了Android客户端和PC服务器端之间的双向通信:
面向UDP的Android——PC双向通信(二):实现Android客户端和PC服务器端的双向通信。但仅仅是传输文本消息。接下来我们想要制定一个协议,来传输我们自定义类的对象。
这里我们考虑模拟订票系统,Android客户端向服务器端提交一个ticket对象,服务器端负责生成一些其他信息,再回传给Android端,实现一个订单的确认。
public class Ticket {
private String name; //购票者姓名
private String ID; //ID
private String SN; //序列号
private String StartStation;//起始站
private String EndStation;//终点站
private int Type; //车票类型
private String Date; //出发日期
private String TrainNumber; //车次
private String StartTime; //起始时刻
private String duration; //时长
private int vehicle; //车厢
private int seat; //座位号
private double price; //票价
//getter、setter方法
... ...
}
服务器端在之前的代码基础上,增加了ticket对象转换为byte数组、byte数组转换为ticket对象的方法。
转换的依据就是我们所制定的协议。
我们暂定的协议如下:
从客户端接收的、即将被转换为ticket对象的byte数组的格式如下:
//属性: 姓名、ID、序列号、起始站、终点站、车票类型、出发日期、车次 | 总
//长度: 6 |6 |8 |8 |8 |4 |6 |6 | 52
/**
* byte数组转换为Ticket对象
* @param data
* @return
* @throws UnsupportedEncodingException
*/
public Ticket BytesToTicket(byte[] data) throws UnsupportedEncodingException{
Ticket t = new Ticket();
t.setName(BytesToString(subBytes(data,0,6)));
t.setID(BytesToString(subBytes(data,6,6)));
t.setSN(BytesToString(subBytes(data,12,8)));
t.setStartStation(BytesToString(subBytes(data,20,8)));
t.setEndStation(BytesToString(subBytes(data,28,8)));
t.setType(BytesToInt(subBytes(data,36,4)));
t.setDate(BytesToString(subBytes(data,40,6)));
t.setTrainNumber(BytesToString(subBytes(data,46,6)));
System.out.println(t.toString());
return t;
}
在接收到的ticket对象基础上,服务器生成起始时刻、时长、票价,分配车厢、座位号,再转换为byte数组:
//属性: 姓名、ID、序列号、起始站、终点站、车票类型、出发日期、车次 、起始时刻、时长、车厢、座位号、票价 |总
//长度: 6 |6 |8 |8 |8 |4 |6 |6 |4 |4 |4 |4 |8 |76
/**
* 把ticket对象转换为要发送的byte数组
* @param t
* @return
* @throws Exception
*/
public byte[] TicketToBytes(Ticket t) throws Exception{
byte[] data=new byte[76];
addBytes(StringToBytes(t.getName(),6),data,0,6);
addBytes(StringToBytes(t.getID(),6),data,6,6);
addBytes(StringToBytes(t.getSN(),8),data,12,8);
addBytes(StringToBytes(t.getStartStation(),8),data,20,8);
addBytes(StringToBytes(t.getEndStation(),8),data,28,8);
addBytes(IntToBytes(t.getType()),data,36,4);
addBytes(StringToBytes(t.getDate(),6),data,40,6);
addBytes(StringToBytes(t.getTrainNumber(),6),data,46,6);
addBytes(StringToBytes(t.getStartTime(),4),data,52,4);
addBytes(StringToBytes(t.getDuration(),4),data,56,4);
addBytes(IntToBytes(t.getVehicle()),data,60,4);
addBytes(IntToBytes(t.getSeat()),data,64,4);
addBytes(DoubleToBytes(t.getPrice()),data,68,8);
return data;
}
再增加一些基础数据类型之间的转换就可以了,服务器端没有遇到什么问题。
在Android端,ticket对象和byte数组之间的相互转换恰好与Java端相反,难度不大,这里不加赘述。
但是我们遇到了其他的问题。
之前实现双向通信仅仅是在MainActivity中进行消息收发。
而如今我们写了两个界面,在第一个界面MainActivity中填写购票信息,并点击提交,跳转第二个界面ReceiveActivity等待确认购票成功的信息。如果想要再次提交新的购票信息,则需要返回第一个界面填写购票信息。
但是重新创建第二个界面会重复执行代码:
receivesocket = new DatagramSocket(9999);
肯定会报错,因为之前的socket连接没有关闭,端口9999被占用。
如何解决socket在不同界面(前后创建ReceiveActivity的代码虽然相同,但实际上是不同的两个界面)之间共享的问题呢?
我们采用单例模式,即使用静态socket对象,这样DatagramSocket在该应用程序中只有一个实例。
在MainActivity中增加代码:
//静态变量
public static DatagramSocket receivesocket=null;
//静态方法
public static DatagramSocket getsocket() throws Exception {
if(receivesocket==null){
receivesocket= new DatagramSocket(9999);
}
return receivesocket;
}
并在ReceiveActivity中更改代码:
receivesocket = MainActivity.getsocket();
至此,我们可以实现一个或多个客户端的购票提交申请,以及接收来自服务器端对应的确认购票成功的消息。但是丢包仍然是比较常见。
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextArea;
public class UDPServer {
public JFrame frame;
public JLabel IPShow;
public JLabel PortShow;
public JTextArea MsgReceive;
public JTextArea MsgSend;
public JButton SendBtn;
public InetAddress ClientAddress;
public String AddressStr;
public int ClientPort;
public DatagramSocket socket;
public ArrayList<Ticket> tickets = new ArrayList<>();
public int cnt=0;
//发送端口号
public int port=9999;
public static void main(String[] args) throws Exception{
UDPServer server = new UDPServer();
server.showUI();
server.receiveMsg();
// String s="北京欢迎你!";
// byte[] test1 = StringToBytes(s,10);
// String test2 = new String(test1);
// String s1 = BytesToString(test1);
// System.out.println("test1: "+test1.toString());
// System.out.println("test2: "+test2);
// System.out.println(s1);
}
/**
* 字符串转换成大小为len的byte数组,并在末尾补占位符‘-’
* @param str 字符串
* @param len byte数组长度
* @return 补全后的byte数组
* @throws Exception
*/
public static byte[] StringToBytes(String str,int len) throws Exception{
byte[] newData = new byte[len];
byte[] oldData = str.getBytes("GBK");
for(int i=0;i<oldData.length&&i<len;i++){
newData[i]=oldData[i];
}
for(int i=oldData.length;i<len;i++){
newData[i]=(byte) '-';
}
return newData;
}
/**
* byte数组转换为字符串,并删除末尾占位符‘-’
* @param oldData 含占位符的byte数组
* @return 新字符串
* @throws UnsupportedEncodingException
*/
public static String BytesToString(byte[] oldData) throws UnsupportedEncodingException{
String newData = new String(oldData,"GBK");
if(newData.contains("-")){
newData = newData.substring(0, newData.indexOf('-'));
}
return newData;
}
/**
* 从byte数组中分割出小的byte数组
* @param oldData 原byte数组
* @param start 切割点的起点
* @param len 切割的长度
* @return 新byte数组
*/
public byte[] subBytes(byte[] oldData,int start,int len){
byte[] newData = new byte[len];
for(int i=0;i<len;i++){
newData[i]=oldData[i+start];
}
return newData;
}
// 姓名、ID、序列号、起始站、终点站、车票类型、出发日期、车次
// 6 |6 |8 |8 |8 |4 |6 |6 | 52
/**
* byte数组转换为Ticket对象
* @param data
* @return
* @throws UnsupportedEncodingException
*/
public Ticket BytesToTicket(byte[] data) throws UnsupportedEncodingException{
Ticket t = new Ticket();
t.setName(BytesToString(subBytes(data,0,6)));
t.setID(BytesToString(subBytes(data,6,6)));
t.setSN(BytesToString(subBytes(data,12,8)));
t.setStartStation(BytesToString(subBytes(data,20,8)));
t.setEndStation(BytesToString(subBytes(data,28,8)));
t.setType(BytesToInt(subBytes(data,36,4)));
t.setDate(BytesToString(subBytes(data,40,6)));
t.setTrainNumber(BytesToString(subBytes(data,46,6)));
System.out.println(t.toString());
return t;
}
/**
* 4位byte数组转化为int型数据
* @param b 4位byte数组
* @return int型数据
*/
public static int BytesToInt(byte[] b){
return b[3]&0xFF|(b[2]&0xFF)<<8|(b[1]&0xFF)<<16|(b[0]&0xFF)<<24;
}
public void showUI(){
frame = new JFrame("UDP Server");
frame.setSize(800, 700);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());
JLabel IPLabel = new JLabel(" 源IP:");
IPLabel.setPreferredSize(new Dimension(80,30));
IPShow = new JLabel("__________________");
IPShow.setPreferredSize(new Dimension(300,30));
JLabel PortLabel = new JLabel("端口:");
PortLabel.setPreferredSize(new Dimension(80,30));
PortShow = new JLabel("__________________");
PortShow.setPreferredSize(new Dimension(300,30));
JLabel RecvLabel = new JLabel("接收到消息:");
RecvLabel.setPreferredSize(new Dimension(700,30));
MsgReceive = new JTextArea();
MsgReceive.setPreferredSize(new Dimension(750,250));
JLabel SendLabel = new JLabel("待发送消息:");
RecvLabel.setPreferredSize(new Dimension(700,30));
MsgSend = new JTextArea();
MsgSend.setPreferredSize(new Dimension(750,250));
SendBtn = new JButton("发送");
SendBtn.setPreferredSize(new Dimension(80,50));
SendBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try {
sendMsg(); //发送消息
} catch (Exception e1) {
e1.printStackTrace();
}
}
});
frame.add(IPLabel); //源IP
frame.add(IPShow);
frame.add(PortLabel); //端口
frame.add(PortShow);
frame.add(RecvLabel); //接收到消息:
frame.add(MsgReceive);
frame.add(SendLabel); //待发送消息:
frame.add(MsgSend);
frame.add(SendBtn); //发送按钮
frame.setVisible(true);
}
/**
* 接收消息,转化为ticket对象,并添加到tickets队列中
* @throws Exception
*/
public void receiveMsg() throws Exception{
System.out.println("UDPServer start...");
socket = new DatagramSocket(9999);
new Thread(){
public void run(){
while(true){
byte[] data = new byte[52];
DatagramPacket request = new DatagramPacket(data, 52);
//byte[] b=new byte[4];
//port=BytesToInt(addBytes());
System.out.println("准备接收消息");
try {
socket.receive(request);
System.out.println("消息接收完毕");
String test = new String(data);
System.out.println("---TEST--接收到的byte数组为:"+test);
//将Ticket对象添加到tickets队列中
tickets.add(BytesToTicket(data));
cnt++;
ClientAddress=request.getAddress();
IPShow.setText(ClientAddress.toString());
System.out.println("客户机IP地址:"+IPShow.getText());
ClientPort=request.getPort();
PortShow.setText(""+ClientPort);
System.out.println("客户机端口:"+PortShow.getText());
String s = new String(data,"GBK");
System.out.println("收到新消息:"+s+"\n"+s.length());
//MsgReceive.setText(s);
MsgReceive.setText(tickets.get(cnt-1).toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
}
// /**
// * 发送消息
// * @throws Exception
// */
// public void sendMsg() throws Exception{
// System.out.println("准备发送消息");
// String Msg = MsgSend.getText();
// System.out.println("要发送的消息是: "+Msg);
// byte data[] = Msg.getBytes("GBK");
// DatagramPacket request =
// new DatagramPacket(data,data.length,ClientAddress,9999);
// socket.send(request);
// System.out.println("发送成功");
// }
/**
* 将byte数组添加到大的byte数组中
* @param myData 要复制 的byte数组
* @param oldData 目的byte数组
* @param start 插入点的起点
* @param len 插入的长度
* @return 新byte数组
*/
public byte[] addBytes(byte[] myData,byte[] oldData,int start,int len){
for(int i=0;i<len;i++){
oldData[i+start]=myData[i];
}
return oldData;
}
// 姓名、 ID、 序列号、 起始站、 终点站、车票类型、出发日期、 车次、 起始时刻、时长、车厢、 座位号、票价
// 6 |6 |8 |8 |8 |4 |6 |6 |4 |4 |4 |4 |8 |76
/**
* 把ticket对象转换为要发送的byte数组
* @param t
* @return
* @throws Exception
*/
public byte[] TicketToBytes(Ticket t) throws Exception{
byte[] data=new byte[76];
addBytes(StringToBytes(t.getName(),6),data,0,6);
addBytes(StringToBytes(t.getID(),6),data,6,6);
addBytes(StringToBytes(t.getSN(),8),data,12,8);
addBytes(StringToBytes(t.getStartStation(),8),data,20,8);
addBytes(StringToBytes(t.getEndStation(),8),data,28,8);
addBytes(IntToBytes(t.getType()),data,36,4);
addBytes(StringToBytes(t.getDate(),6),data,40,6);
addBytes(StringToBytes(t.getTrainNumber(),6),data,46,6);
addBytes(StringToBytes(t.getStartTime(),4),data,52,4);
addBytes(StringToBytes(t.getDuration(),4),data,56,4);
addBytes(IntToBytes(t.getVehicle()),data,60,4);
addBytes(IntToBytes(t.getSeat()),data,64,4);
addBytes(DoubleToBytes(t.getPrice()),data,68,8);
return data;
}
/**
* 将int型数据转换成byte数组
* @param a
* @return
*/
public byte[] IntToBytes(int a) {
byte[] b = new byte[4];
b[0] = (byte)((a>>24)&0xFF);
b[1] = (byte)((a>>16)&0xFF);
b[2] = (byte)((a>>8)&0xFF);
b[3] = (byte)(a&0xFF);
return b;
}
public byte[] DoubleToBytes(double d){
long value = Double.doubleToLongBits(d);
byte[] b = new byte[8];
for(int i=0;i<8;i++){
b[i]=(byte)((value>>8*i)&0xff);
}
return b;
}
/**
* 生成起始时刻、时长、车厢、座位、票价
* @param t
*/
public void handleTicket(Ticket t){
Random r = new Random();
t.setStartTime("1258");
t.setDuration("0215");
t.setVehicle(r.nextInt(16)+1);
t.setSeat(r.nextInt(100)+1);
t.setPrice(12.5);
System.out.println(t.toString());
}
/**
* 发送ticket对象
* @throws Exception
*/
public void sendMsg() throws Exception{
System.out.println("准备发送消息");
Ticket t = tickets.get(cnt-1);
handleTicket(t);
MsgSend.setText(t.toString());
byte[] data = TicketToBytes(t);
//DatagramSocket sendSocket=new DatagramSocket(port);
DatagramPacket request =
new DatagramPacket(data,data.length,ClientAddress,port);
socket.send(request);
System.out.println("发送成功");
}
}
//MainActivity.java
import java.net.DatagramSocket;
import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends Activity {
public static DatagramSocket receivesocket=null;
public static DatagramSocket getsocket() throws Exception {
if(receivesocket==null){
receivesocket= new DatagramSocket(9999);
}
return receivesocket;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText name=(EditText)this.findViewById(R.id.nametext);
EditText ID=(EditText)this.findViewById(R.id.IDtext);
EditText start=(EditText)this.findViewById(R.id.starttext);
EditText end=(EditText)this.findViewById(R.id.endtext);
EditText type=(EditText)this.findViewById(R.id.typetext);
EditText date=(EditText)this.findViewById(R.id.datetext);
EditText number=(EditText)this.findViewById(R.id.numbertext);
Button submit=(Button)this.findViewById(R.id.submit);
ButtonListener bl=new ButtonListener(this,name,ID,start,end,type,date,number);
submit.setOnClickListener(bl);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}
//ReceiveActivity.java
package com.example.udpsend;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.Menu;
import android.widget.TextView;
public class ReceiveActivity extends Activity {
public DatagramSocket receivesocket;
public byte[] msg;
public TextView show;
public int port;
public byte[] value;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_receive);
show=(TextView)this.findViewById(R.id.show);
new Thread(){
public void run(){
try {
receivesocket=MainActivity.getsocket();
//receivesocket = new DatagramSocket(9999);
while(true){
receive();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
public void receive() throws Exception {
byte[] data = new byte[76];
DatagramPacket receivepacket = new DatagramPacket(data, 76);
receivesocket.receive(receivepacket);
//receivesocket.close();
Ticket tiket=rechange(data);
String s=showsetText(tiket);
Message msg=hand.obtainMessage();
msg.obj =s;
hand.sendMessage(msg);
}
public Handler hand=new Handler(){
public void handleMessage(Message msg) {
String s=(String)msg.obj;
show.setText(s);
}
};
public String showsetText(Ticket tiket){
String s=tiket.getName()+"¶©Æ±³É¹¦£¡"+"\nÏêϸÐÅÏ¢ÈçÏ£º\nÐÕÃû£º"+tiket.getName()+"\nID£º"+tiket.getID()+"\nÈÕÆÚ£º"+
tiket.getDate()+" "+tiket.getStartTime()+"\n´Ó£º"+tiket.getStartStation()+"³ö·¢µ½"+tiket.getEndStation()+
"\nʱ³¤£º"+tiket.getDuration()+"\n³µÏáºÅ£º"+tiket.getVehicle()+" ×ùλºÅ£º"+tiket.getSeat()+"\nƱ¼Û£º"+tiket.getPrice()+
"\n\n´ËÏûÏ¢ÐòÁкţº"+tiket.getSN();
return s;
}
public byte[] convert(int start,int l,byte[] msg){
byte[] c=new byte[l];
for(int i=start;i<start+l;i++)
c[i-start]=msg[i];
return c;
}
public Ticket rechange(byte[] msg) throws UnsupportedEncodingException{
String name=BytesToString(convert(0,6,msg));
String ID=BytesToString(convert(6,6,msg));
String SN=BytesToString(convert(12,8,msg));
String StartStation=BytesToString(convert(20,8,msg));
String EndStation=BytesToString(convert(28,8,msg));
int Type=byteArrayToInt(convert(36,4,msg));
String Date=BytesToString(convert(40,6,msg));
String TrainNumber=BytesToString(convert(46,6,msg));
String StartTime=BytesToString(convert(52,4,msg));
String duration=BytesToString(convert(56,4,msg));
int vehicle=byteArrayToInt(convert(60,4,msg));
int seat=byteArrayToInt(convert(64,4,msg));
double price=bytes2Double(convert(68,8,msg));
Ticket tiket=new Ticket(name,ID,SN,StartStation,EndStation,Type,Date,TrainNumber,StartTime,duration,
vehicle,seat,price);
return tiket;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.receive, menu);
return true;
}
public static int byteArrayToInt(byte[] b) {
return b[3] & 0xFF |
(b[2] & 0xFF) << 8 |
(b[1] & 0xFF) << 16 |
(b[0] & 0xFF) << 24;
}
public static String BytesToString(byte[] oldData) throws UnsupportedEncodingException{
String newData = new String(oldData,"GBK");
if(newData.contains("-")){
newData = newData.substring(0, newData.indexOf('-'));
}
return newData;
}
public static double bytes2Double(byte[] arr) {
long value = 0;
for (int i = 0; i < 8; i++) {
value |= ((long) (arr[i] & 0xff)) << (8 * i);
}
return Double.longBitsToDouble(value);
}
}