基於Socket的TCP長連線(服務端Java+客戶端Android),Service配合AIDL實現
最近公司的專案要求加入訊息推送功能,由於專案使用者量不是很大,推送需求不是很嚴格,而且是基於內網的推送,所以我捨棄了使用三方的推送服務,自己使用Socket寫了推送功能,剪出一個小Demo來跟大家分享一下,有不足之處希望讀者能夠多多給出建議。
關於Socket的TCP和UDP協議,相信大家都是很清楚的,當然做長連線兩者都是可以的,據說QQ和微信360等使用的UDP做的,使用兩個Service相互監控保持不被系統殺死,但是我目前做不了他們那樣的效果,如果各位有更好的辦法,可以共享一下,我們言歸正傳。
程式碼還是相當粗糙的,目的是練習使用Socket。伺服器端使用一個執行緒的集合儲存不同的客戶端,相關程式碼如下:
public class ServerDemo { private int count = 0; private boolean isStartServer = false; private ArrayList<SocketThread> mThreadList = new ArrayList<SocketThread>(); public static void main(String[] args) throws IOException { ServerDemo server = new ServerDemo(); server.start(); } /** * 開啟服務端的Socket * @throws IOException */ public void start() throws IOException { // 啟動服務ServerSocket,設定埠號 ServerSocket ss = new ServerSocket(9001); System.out.println("服務端已開啟,等待客戶端連線:"); isStartServer = true; int socketID = 0; Socket socket = null; startMessageThread(); while (isStartServer) { // 此處是一個阻塞方法,當有客戶端連線時,就會呼叫此方法 socket = ss.accept(); System.out.println("客戶端連線成功" + socket.getInetAddress()); // 4. 為這個客戶端的Socket資料連線 SocketThread thread = new SocketThread(socket, socketID++); thread.start(); mThreadList.add(thread); } } private void startMessageThread() { new Timer().schedule(new TimerTask() { @Override public void run() { try { for (SocketThread st : mThreadList) {// 分別向每個客戶端傳送訊息 if (st.socket == null || st.socket.isClosed()) continue; System.out.println("客戶端的userId:" + st.userId + " 訊息編號:" + count); if (st.userId == null || "".equals(st.userId))// 如果暫時沒有確定Socket對應的使用者Id先不發 continue; String content = "我是從伺服器發來的訊息:" + count; // 根據userId模擬服務端向不同的客戶端推送訊息 if (count % 2 == 0) { if (st.userId.equals("002")) content = "我是從伺服器發發送給使用者002的訊息:" + count; else continue; } else { if (st.userId.equals("001")) content = "我是從伺服器發發送給使用者001的訊息:" + count; else continue; } SocketMessage message = new SocketMessage(); message.setFrom(Custom.NAME_SERVER); message.setTo(Custom.NAME_CLIENT); message.setMessage(content); message.setType(Custom.MESSAGE_EVENT); message.setUserId(st.userId); BufferedWriter writer = st.writer; String jMessage = Util.initJsonObject(message).toString() + "\n"; writer.write(jMessage); writer.flush(); System.out.println("向客戶端" + st.socket.getInetAddress() + "傳送了訊息:" + content); } count++; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }, 0, 1000 * 30);//此處設定定時器目的是模仿服務端向客戶端推送訊息,假定每隔30秒推送一條訊息 } /** * 關閉與SocketThread所代表的客戶端的連線 * @param socketThread要關閉的客戶端 * @throws IOException */ private void closeSocketClient(SocketThread socketThread) throws IOException { if (socketThread.socket != null && !socketThread.socket.isClosed()) { if (socketThread.reader != null) socketThread.reader.close(); if (socketThread.writer != null) socketThread.writer.close(); socketThread.socket.close(); } mThreadList.remove(socketThread); socketThread = null; } /** * 客戶端Socket執行緒, * @author 華碩 * */ public class SocketThread extends Thread { public int socketID; public Socket socket;//客戶端的Socket public BufferedWriter writer; public BufferedReader reader; public String userId;//客戶端的UserId private long lastTime; public SocketThread(Socket socket, int count) { socketID = count; this.socket = socket; lastTime = System.currentTimeMillis(); } @Override public void run() { super.run(); try { reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); //迴圈監控讀取客戶端發來的訊息 while (isStartServer) { // 超出了傳送心跳包規定時間,說明客戶端已經斷開連線了這時候要斷開與該客戶端的連線 long interval = System.currentTimeMillis() - lastTime; if (interval >= (Custom.SOCKET_ACTIVE_TIME * 1000 * 4)) { System.out.println("客戶端發包間隔時間嚴重延遲,可能已經斷開了interval:" + interval); System.out.println("Custom.SOCKET_ACTIVE_TIME * 1000:" + Custom.SOCKET_ACTIVE_TIME * 1000); closeSocketClient(this); break; } if (reader.ready()) { lastTime = System.currentTimeMillis(); System.out.println("收到訊息,準備解析:"); String data = reader.readLine(); System.out.println("解析成功:" + data); SocketMessage from = Util.parseJson(data); //給UserID賦值,此處是我們專案的需求,根據客戶端不同的UserId來分別進行推送 if (userId == null || "".equals(userId)) userId = from.getUserId(); SocketMessage to = new SocketMessage(); if (from.getType() == Custom.MESSAGE_ACTIVE) {//心跳包 System.out.println("收到心跳包:" + socket.getInetAddress()); to.setType(Custom.MESSAGE_ACTIVE); to.setFrom(Custom.NAME_SERVER); to.setTo(Custom.NAME_CLIENT); to.setMessage(""); to.setUserId(userId); writer.write(Util.initJsonObject(to).toString() + "\n"); writer.flush(); } else if (from.getType() == Custom.MESSAGE_CLOSE) {//關閉包 System.out.println("收到斷開連線的包:" + socket.getInetAddress()); to.setType(Custom.MESSAGE_CLOSE); to.setFrom(Custom.NAME_SERVER); to.setTo(Custom.NAME_CLIENT); to.setMessage(""); to.setUserId(userId); writer.write(Util.initJsonObject(to).toString() + "\n"); writer.flush(); closeSocketClient(this); break; } else if (from.getType() == Custom.MESSAGE_EVENT) {//事件包,客戶端可以向服務端傳送自定義訊息 System.out.println("收到普通訊息包:" + from.getMessage()); } } Thread.sleep(100); } } catch (Exception e) { e.printStackTrace(); } } } }
當服務端收到客戶端的連線時候,就開啟執行緒用來接收客戶端的訊息(心跳包的訊息或者是普通的訊息),伺服器端模擬訊息推送,利用定時器每隔一段時間就像客戶端推送一條訊息。我的訊息分為三種:
心跳訊息,客戶端每隔一段時間就會向伺服器傳送一條心跳訊息,服務端收到之後會馬上返回一條心跳訊息給客戶端,以此來保持連線。
普通訊息,客戶端與服務端的資料互動。
斷開連線訊息,當服務端收到客戶端的斷開連線訊息時,會立馬給客戶端返回斷開連線訊息,同時服務端會主動與客戶端斷開連線,並從執行緒任務列表把該客戶端Socket移除,只有再次連線才會被新增進來。客戶端收到服務端的斷開回執,也會斷開連線,防止資源浪費和異常發生。
因為我專業是做Android開發的,重點說一下客戶端的程式碼:
使用Service的持久存活來保持Socket不被意外斷開
在onStartCommand方法返回START_STICKY,如果Service由於記憶體不夠被意外殺死,那麼等記憶體夠了之後,還會重啟
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand(Intent intent, int flags, int startId)");
// return super.onStartCommand(intent, flags, startId);
//當Service重啟時候,判斷一下,如果Socket斷開了連線,但是儲存的狀態還是連線狀態,那就是意外的斷開,需要重連
if (isServerClose() && SocketServiceSP.getInstance(SocketService.this).isSocketConnect())
connectSocket();
return START_STICKY;//設定START_STICKY為了使服務被意外殺死後可以重啟
}
註冊廣播,當Service走onDestroy()方法時候,傳送廣播,重啟服務
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate()");
SocketServiceSP.getInstance(this).saveSocketServiceStatus(true);//儲存了Service的開啟狀態
//收到Service被殺死的廣播,立即重啟
restartBR = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.e(TAG, "SocketServer重啟了......");
String action = intent.getAction();
if (!TextUtils.isEmpty(action) && action.equals("socketService_killed")) ;
Intent sIntent = new Intent(SocketService.this, SocketService.class);
startService(sIntent);
SocketServiceSP.getInstance(SocketService.this).saveSocketServiceStatus(true);//儲存了Service的開啟狀態
//當Service重啟時候,判斷一下,如果Socket斷開了連線,但是儲存的狀態還是連線狀態,那就是意外的斷開,需要重連
if (isServerClose() && SocketServiceSP.getInstance(SocketService.this).isSocketConnect())
connectSocket();
}
};
registerReceiver(restartBR, new IntentFilter("socketService_killed"));
}
/**
* 銷燬Service同時要銷燬Socket
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy()");
if (mReadThread != null)
mReadThread.release();
releaseLastSocket(mSocket);
sendBroadcast(new Intent("socketService_killed"));
SocketServiceSP.getInstance(SocketService.this).saveSocketServiceStatus(false);
unregisterReceiver(restartBR);
}
Service的啟動使用的startService方法啟動的,為了使使用者能夠操作介面來連線和斷開Socket,就需要Service程序與Activity程序進行互動,而這裡需要用到AIDL技術,AIDL介面的方法引數使用了自定義的SocketMessage類,所以還需要實現Parcelable介面。
關於AIDL的使用,就不進行詳細的介紹了,有不清楚的朋友可以私下交流
下面看一下整個Service的程式碼
package com.herenit.socketpushdemo.core;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.herenit.socketpushdemo.ISocketMessageListener;
import com.herenit.socketpushdemo.ISocketServiceInterface;
import com.herenit.socketpushdemo.MainActivity;
import com.herenit.socketpushdemo.R;
import com.herenit.socketpushdemo.bean.SocketMessage;
import com.herenit.socketpushdemo.common.Custom;
import com.herenit.socketpushdemo.common.SocketServiceSP;
import com.herenit.socketpushdemo.common.Util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.ref.WeakReference;
import java.net.Socket;
/**
* Created by HouBin on 2017/3/14.
* 與伺服器保持長連線的Service
*/
public class SocketService extends Service {
private final String TAG = SocketService.class.getSimpleName();
//Service例項,用於在Activity中進行連線斷開發訊息等圖形介面化的操作
//Socket的弱引用
private WeakReference<Socket> mSocket;
//訊息發出的時間(不管是心跳包還是普通訊息,傳送完就會跟新時間)
private long sendTime = 0;
private final int MSG_WHAT_CONNECT = 111; //連線Socket
private final int MSG_WHAT_DISCONNECT = 112;//斷開Socket
private final int MSG_WHAT_SENDMESSAGE = 113;//傳送訊息
/**
* 處理Socket的連線斷開發訊息的Handler機制
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_WHAT_CONNECT:
if (isServerClose()) {
connectSocket();
} else {
Toast.makeText(SocketService.this, "Socket已經連線上了", Toast.LENGTH_SHORT).show();
}
break;
case MSG_WHAT_DISCONNECT:
if (isServerClose())
Toast.makeText(SocketService.this, "Socket已經斷開了", Toast.LENGTH_SHORT).show();
else
interruptSocket();
break;
case MSG_WHAT_SENDMESSAGE:
if (isServerClose()) {
Toast.makeText(SocketService.this, "請先連線Socket", Toast.LENGTH_SHORT).show();
} else {
SocketMessage socketMessage = (SocketMessage) msg.obj;
try {
SocketService.this.sendMessage(socketMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
}
break;
}
}
};
//讀取伺服器端發來的訊息的執行緒
private ReadThread mReadThread;
//監控服務被殺死重啟的廣播,保持服務不被殺死
private BroadcastReceiver restartBR;
public SocketService() {
super();
}
/**
* 建立Service的同時要建立Socket
*/
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate()");
SocketServiceSP.getInstance(this).saveSocketServiceStatus(true);//儲存了Service的開啟狀態
//收到Service被殺死的廣播,立即重啟
restartBR = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.e(TAG, "SocketServer重啟了......");
String action = intent.getAction();
if (!TextUtils.isEmpty(action) && action.equals("socketService_killed")) ;
Intent sIntent = new Intent(SocketService.this, SocketService.class);
startService(sIntent);
SocketServiceSP.getInstance(SocketService.this).saveSocketServiceStatus(true);//儲存了Service的開啟狀態
//當Service重啟時候,判斷一下,如果Socket斷開了連線,但是儲存的狀態還是連線狀態,那就是意外的斷開,需要重連
if (isServerClose() && SocketServiceSP.getInstance(SocketService.this).isSocketConnect())
connectSocket();
}
};
registerReceiver(restartBR, new IntentFilter("socketService_killed"));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand(Intent intent, int flags, int startId)");
// return super.onStartCommand(intent, flags, startId);
//當Service重啟時候,判斷一下,如果Socket斷開了連線,但是儲存的狀態還是連線狀態,那就是意外的斷開,需要重連
if (isServerClose() && SocketServiceSP.getInstance(SocketService.this).isSocketConnect())
connectSocket();
return START_STICKY;//設定START_STICKY為了使服務被意外殺死後可以重啟
}
/**
* 客戶端通過Socket與服務端建立連線
*/
public void connectSocket() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Socket socket = new Socket(Custom.SERVER_HOST, Custom.SERVER_PORT);
//這裡儲存Socket當前已經連線上了
SocketServiceSP.getInstance(SocketService.this).saveSocketConnectStatus(true);
socket.setSoTimeout(Custom.SOCKET_CONNECT_TIMEOUT * 1000);
Log.e(TAG, "Socket連線成功。。。。。。");
mSocket = new WeakReference<Socket>(socket);
mReadThread = new ReadThread(socket);
mReadThread.start();
mHandler.postDelayed(activeRunnable, Custom.SOCKET_ACTIVE_TIME * 1000);//開啟定時器,定時傳送心跳包,保持長連線
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 傳送心跳包的任務
*/
private Runnable activeRunnable = new Runnable() {
@Override
public void run() {
if (System.currentTimeMillis() - sendTime >= Custom.SOCKET_ACTIVE_TIME * 1000) {
SocketMessage message = new SocketMessage();
message.setType(Custom.MESSAGE_ACTIVE);
message.setMessage("");
message.setFrom(Custom.NAME_CLIENT);
message.setTo(Custom.NAME_SERVER);
try {
if (!sendMessage(message)) {
if (mReadThread != null)
mReadThread.release();
releaseLastSocket(mSocket);
connectSocket();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
mHandler.postDelayed(this, Custom.SOCKET_ACTIVE_TIME * 1000);
}
};
/**
* 傳送訊息到服務端
*
* @param message
* @return
*/
public boolean sendMessage(SocketMessage message) throws RemoteException {
message.setUserId("001");
if (mSocket == null || mSocket.get() == null) {
return false;
}
Socket socket = mSocket.get();
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
if (!socket.isClosed()) {
String jMessage = Util.initJsonObject(message).toString() + "\n";
writer.write(jMessage);
writer.flush();
Log.e(TAG, "傳送訊息:" + jMessage);
sendTime = System.currentTimeMillis();//每次傳送成資料,就改一下最後成功傳送的時間,節省心跳間隔時間
if (message.getType() == Custom.MESSAGE_EVENT) {//通知實現了訊息監聽器的介面,讓其跟新訊息列表
messageListener.updateMessageList(message);
}
} else {
return false;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 讀取訊息的執行緒
*/
class ReadThread extends Thread {
private WeakReference<Socket> mReadSocket;
private boolean isStart = true;
public ReadThread(Socket socket) {
mReadSocket = new WeakReference<Socket>(socket);
}
public void release() {
isStart = false;
releaseLastSocket(mReadSocket);
}
@Override
public void run() {
super.run();
Socket socket = mReadSocket.get();
if (socket != null && !socket.isClosed()) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while (isStart) {
if (reader.ready()) {
String message = reader.readLine();
Log.e(TAG, "收到訊息:" + message);
SocketMessage sMessage = Util.parseJson(message);
if (sMessage.getType() == Custom.MESSAGE_ACTIVE) {//處理心跳回執
} else if (sMessage.getType() == Custom.MESSAGE_EVENT) {//事件訊息
if (messageListener != null)
messageListener.updateMessageList(sMessage);
sendNotification(sMessage);
} else if (sMessage.getType() == Custom.MESSAGE_CLOSE) {//斷開連線訊息回執
mHandler.removeCallbacks(activeRunnable);
release();
releaseLastSocket(mSocket);
}
}
Thread.sleep(100);//每隔0.1秒讀取一次,節省點資源
}
} catch (IOException e) {
release();
releaseLastSocket(mSocket);
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
//通知的ID,為了分開顯示,需要根據Id區分
private int nId = 0;
/**
* 收到時間訊息,傳送通知提醒
*
* @param sMessage
*/
private void sendNotification(SocketMessage sMessage) {
//為了版本相容,使用v7包的BUILDER
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
//狀態列顯示的提示,有的手機不顯示
builder.setTicker("簡單的Notification");
//通知欄標題
builder.setContentTitle("from" + sMessage.getFrom() + "的訊息");
//通知欄內容
builder.setContentText(sMessage.getMessage());
//通知內容摘要
builder.setSubText(sMessage.getUserId());
//在通知右側的時間下面用來展示一些其他資訊
// builder.setContentInfo("其他");
//用來顯示同種通知的數量,如果設定了ContentInfo屬性,則NUmber屬性會被覆蓋,因為二者顯示的位置相同
// builder.setNumber(3);
//可以點選通知欄的刪除按鈕
builder.setAutoCancel(true);
//系統狀態列顯示的小圖示
builder.setSmallIcon(R.mipmap.jpush_notification_icon);
//通知下拉顯示的大圖示
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
//點選通知跳轉的INTENT
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, intent, 0);
builder.setContentIntent(pendingIntent);//點選跳轉
//通知預設的聲音,震動,呼吸燈
builder.setDefaults(NotificationCompat.DEFAULT_ALL);
Notification notification = builder.build();
NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(nId, notification);
nId++;
}
/**
* 釋放Socket,並關閉
*
* @param socket
*/
private void releaseLastSocket(WeakReference<Socket> socket) {
//正常的斷開連線,這裡儲存斷開連線的狀態
SocketServiceSP.getInstance(this).saveSocketConnectStatus(false);
if (socket != null) {
Socket so = socket.get();
try {
if (so != null && !so.isClosed())
so.close();
socket.clear();
Log.e(TAG, "Socket斷開連線。。。。。。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 判斷是否斷開連線,斷開返回true,沒有返回false
*
* @return
*/
public boolean isServerClose() {
try {
if (mSocket != null && mSocket.get() != null) {
mSocket.get().sendUrgentData(0);//傳送1個位元組的緊急資料,預設情況下,伺服器端沒有開啟緊急資料處理,不影響正常通訊
return false;
}
} catch (Exception se) {
return true;
}
return true;
}
/**
* 銷燬Service同時要銷燬Socket
*/
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy()");
if (mReadThread != null)
mReadThread.release();
releaseLastSocket(mSocket);
sendBroadcast(new Intent("socketService_killed"));
SocketServiceSP.getInstance(SocketService.this).saveSocketServiceStatus(false);
unregisterReceiver(restartBR);
}
/**
* 對外提供的斷開Socket連線的方法(向伺服器傳送斷開的包,伺服器收到後會與之斷開)
*/
public void interruptSocket() {
new Thread(new Runnable() {
@Override
public void run() {
SocketMessage message = new SocketMessage();
message.setType(Custom.MESSAGE_CLOSE);
message.setMessage("");
message.setFrom(Custom.NAME_CLIENT);
message.setTo(Custom.NAME_SERVER);
try {
sendMessage(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}).start();
}
@Override
public boolean onUnbind(Intent intent) {
Log.e(TAG, "onUnbind(Intent intent)");
return super.onUnbind(intent);
}
@Override
public void onRebind(Intent intent) {
Log.e(TAG, "onBind(Intent intent)");
super.onRebind(intent);
}
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG, "onRebind(Intent intent) ");
return mBinder;
}
/**************************************************
* AIDL
********************************************************/
private ISocketMessageListener messageListener;
private Binder mBinder = new ISocketServiceInterface.Stub() {
private static final String ITAG = "ISocketServiceInterface";
/**
* 客戶端要求連線Socket
* @throws RemoteException
*/
@Override
public void connectSocket() throws RemoteException {
Log.e(ITAG, "connectSocket");
mHandler.sendEmptyMessage(MSG_WHAT_CONNECT);
}
/**
* 客戶端要求斷開Socket連線
* @throws RemoteException
*/
@Override
public void disConnectSocket() throws RemoteException {
Log.e(ITAG, "disConnectSocket");
mHandler.sendEmptyMessage(MSG_WHAT_DISCONNECT);
}
/**
* 客戶端向伺服器端傳送訊息
* @param message
* @throws RemoteException
*/
@Override
public void sendMessage(SocketMessage message) throws RemoteException {
Log.e(ITAG, "sendMessage");
Message msg = Message.obtain();
msg.what = MSG_WHAT_SENDMESSAGE;
msg.obj = message;
mHandler.sendMessage(msg);
}
/**
* 客戶端新增訊息監聽器,監聽伺服器端發來的訊息
* @param listener
* @throws RemoteException
*/
@Override
public void addMessageListener(ISocketMessageListener listener) throws RemoteException {
Log.e(ITAG, "addMessageListener");
messageListener = listener;
}
@Override
public void removeMessageListener(ISocketMessageListener listener) throws RemoteException {
Log.e(ITAG, "removeMessageListener");
messageListener = null;
}
};
}
程式碼的註釋還是比較詳細的,整體的程式碼大家可以去我的GitHub上下載
服務端程式碼
客戶端程式碼
時間比較緊,介紹的不是很詳細,很抱歉,大家有疑問就直接看整個專案或者直接聯絡我吧!