一個簡單的自定義通訊協議 socket
這是轉自javaeye的一篇文章,作者是vtrtbb。
按照網路通訊的傳統,我們都會自定義協議,這有很多好處,大家可以自己體會(嘿嘿)。
一直不知道socket通訊時候自定義資料包是什麼樣子的,偶然做了個小例子。
先來說說資料包的定義,我這裡是包頭+內容 組成的:其中包頭內容分為包型別+包長度, 那就是 訊息物件=包型別+包長度+訊息體
包型別 byte 型
包長度 int 型
訊息體 byte[]
包總長度為 1 + 4 + 訊息體.getBytes().length
發包方法如下:
[java] view plain copy print?- privatevoid sendTextMsg(DataOutputStream out,String msg )
- byte[] bytes= msg.getBytes();
- int totalLen = 1 + 4 + bytes.length;
- out.writeByte(1);
- out.writeInt(totalLen);
- out.write(bytes);
- out.flush();
- }
客戶端傳送訊息類為:
- import java.io.DataOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.net.Socket;
- import java.net.UnknownHostException;
- import java.util.Scanner;
- publicclass MsgClient {
- private DataOutputStream outs;
- publicstaticvoid main(String[] args) {
- try {
- MsgClient client = new MsgClient();
- client.connServer("127.0.0.1", 9292);
- } catch (UnknownHostException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- privatevoid sendTextMsg(DataOutputStream out,String msg ) throws IOException {
- byte[] bytes= msg.getBytes();
- int totalLen = 1 + 4 + bytes.length;
- out.writeByte(1);
- out.writeInt(totalLen);
- out.write(bytes);
- out.flush();
- }
- publicvoid connServer(String ip,int port) throws UnknownHostException, IOException {
- Socket client = new Socket(ip,port);
- InputStream in = client.getInputStream();
- OutputStream out = client.getOutputStream();
- outs = new DataOutputStream(out);
- while(true) {
- Scanner scaner = new Scanner(System.in);
- sendTextMsg(outs, "測試消");
- }
- }
服務端接收類為:
[java] view plain copy print?- import java.io.DataInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.net.ServerSocket;
- import java.net.Socket;
- publicclass MsgServer {
- publicstaticvoid main(String[] args) {
- try {
- MsgServer server = new MsgServer();
- server.setUpServer(9090);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- publicvoid setUpServer(int port) throws IOException {
- ServerSocket server = new ServerSocket(port);
- while(true) {
- Socket client = server.accept();
- System.out.println("客戶端IP:"+client.getRemoteSocketAddress());
- processMesage(client);
- }
- }
- privatevoid processMesage(Socket client) throws IOException {
- InputStream ins = client.getInputStream();
- DataInputStream dins = new DataInputStream(ins);
- //服務端解包過程
- while(true) {
- int totalLen = dins.readInt();
- byte flag = dins.readByte();
- System.out.println("接收訊息型別"+flag);
- byte[] data = newbyte[totalLen - 4 - 1];
- dins.readFully(data);
- String msg = new String(data);
- System.out.println("發來的內容是:"+msg);
- }
- }
- }
這樣就基本完成了,但實際還有好多問題,比如說服務端用如何用多執行緒服務來完成客戶端的請求已提高效率,如果是NIO方式怎麼來實現?多個訊息型別時候怎麼抽象?這些都沒有考慮
另外有兩個開源的框架不錯,一個是apache mina 還有個是netty ,有機會試試。
另一篇文章中敘述:
------------------
TCP Socket協議定義
------------------
本文從這裡開始,主要介紹TCP的socket程式設計。
新手們(例如當初的我),第一次寫socket,總是以為在傳送方壓入一個"Helloworld",接收方收到了這個字串,就“精通”了Socket程式設計了。而實際上,這種程式設計根本不可能用在現實專案,因為:
1. socket在傳輸過程中,helloworld有可能被拆分了,分段到達客戶端),例如 hello + world,一個分段就是一個包(Package),這個就是分包問題。
2. socket在傳輸過成功,不同時間傳送的資料包有可能被合併,同時到達了客戶端,這個就是黏包問題。例如傳送方傳送了hello+world,而接收方可能一次就接受了helloworld.
3. socket會自動在每個包後面補n個 0x0 byte,分割包。具體怎麼去補,這個我就沒有深入瞭解。
4. 不同的資料型別轉化為byte的長度是不同的,例如int轉為byte是4位(int32),這樣我們在製作socket協議的時候要特別小心了。具體可以使用以下程式碼去測試:
程式碼publicvoid test() { int myInt =1; byte[] bytes =newbyte[1024]; BinaryWriter writer =new BinaryWriter(new MemoryStream(bytes)); writer.Write(myInt); writer.Write("j"); writer.Close(); }儘管socket環境如此惡劣,但是TCP的連結也至少保證了:
- 包傳送順序在傳輸過程中是不會改變的,例如傳送方傳送 H E L L,那麼接收方一定也是順序收到H E L L,這個是TCP協議承諾的,因此這點成為我們解決分包、黏包問題的關鍵。
- 如果傳送方傳送的是helloworld, 傳輸過程中分割成為hello+world,那麼TCP保證了在hello與world之間沒有其他的byte。但是不能保證helloworld和下一個命令之間沒有其他的byte。
因此,如果我們要使用socket程式設計,就一定要編寫自己的協議。目前業界主要採取的協議定義方式是:包頭+包體長度+包體。具體如下:
1. 一般包頭使用一個int定義,例如int = 173173173;作用是區分每一個有效的資料包,因此我們的伺服器可以通過這個int去切割、合併包,組裝出完整的傳輸協議。有人使用回車字元去分割包體,例如常見的SMTP/POP協議,這種做法在特定的協議是沒有問題的,可是如果我們傳輸的資訊內容自帶了回車字串,那麼就糟糕了。所以在設計協議的時候要特別小心。
2. 包體長度使用一個int定義,這個長度表示包體所佔的位元流長度,用於伺服器正確讀取並分割出包。
3. 包體就是自定義的一些協議內容,例如是對像序列化的內容(現有的系統已經很常見了,使用物件序列化、反序列化能夠極大簡化開發流程,等版本穩定後再轉入手工壓入byte操作)。
一個實際編寫的例子:比如我要傳輸2個整型 int = 1, int = 2,那麼實際傳輸的資料包如下:
173173173 8 1 2
|------包頭------|----包體長度----|--------包體--------|
這個資料包就是4個整型,總長度 = 4*4 = 16。