Android | UDP的C(Java|Android)/S(Java)通信實戰(zhàn)案例(簡易聊天室)

案例GitHub地址

創(chuàng)建UDP服務(wù)端

  • new一個Module:

    模塊名為:sample

  • 創(chuàng)建一個package镰矿,名為udp:


InetAddress.InetAddressHolder源碼:

InetAddressHolder(String hostName, int address, int family) {
            this.originalHostName = hostName;
            this.hostName = hostName;
            this.address = address;
            this.family = family;
        }
  • 在包下創(chuàng)建UdpServer.class供汛,編寫:
package com.lwp.sample.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;

/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/27 17:08
 *     desc   :
 * </pre>
 */
public class UdpServer {

    private InetAddress mInetAddress;
    private int mPort = 7777;//盡可能用5000以后的

    private DatagramSocket mSocket;

    private Scanner mScanner;

    //構(gòu)造方法中初始化
    public UdpServer() {
        try {
            mInetAddress = InetAddress.getLocalHost();

            //傳入权均,設(shè)置好本服務(wù)器ip 和 本服務(wù)程序指定的端口,虛擬“鏈接”的服務(wù)器一端
            mSocket = new DatagramSocket(mPort, mInetAddress);

            //用于控制面板的輸入
            mScanner = new Scanner(System.in);
            mScanner.useDelimiter("\n");//指定控制面板的輸入以換行來結(jié)束

        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }

    public void start() {

        //讓Server端持續(xù)運行
        while (true) {

            try {
                //類似于緩存區(qū)的一個字節(jié)數(shù)組
                //UDP每次通信的數(shù)據(jù)大小受限制
                //限制就來自于服務(wù)端傳給DatagramPacket的字節(jié)數(shù)組
                //因為UDP是通過DatagramPacket封裝數(shù)據(jù)的钞脂,
                // 而DatagramPacket的創(chuàng)建必須傳入一個字節(jié)數(shù)組燥狰,這個數(shù)組便是通信數(shù)據(jù)包的大小限制
                //
                //這里指定的是1024棘脐,也就是客戶端發(fā)送過來的數(shù)據(jù)包,
                // 每次不能超過1024個字節(jié)龙致,1byte = 8bit
                byte[] buf = new byte[1024];

                //接收客戶端數(shù)據(jù)
                DatagramPacket receivedPacket = new DatagramPacket(buf, buf.length);

                //如果沒有數(shù)據(jù)包到來的話蛀缝,程序會一直阻塞在receive()這里,receive()會阻塞目代,
                // 如果有一個客戶端發(fā)送一個數(shù)據(jù)包到這個程序中屈梁,
                // 程序就會去執(zhí)行receive()方法,將接收到的數(shù)據(jù)傳輸?shù)絩eceivedPacket中進而傳輸給receive()
                mSocket.receive(receivedPacket);
                //所以如果程序能往下走榛了,就證明接收到數(shù)據(jù)了


                //拿到客戶端地址在讶、端口號、發(fā)送過來的數(shù)據(jù)
                InetAddress address = receivedPacket.getAddress();
                int port = receivedPacket.getPort();
                byte[] data = receivedPacket.getData();
                String clientMsg = new String(data, 0, data.length);//把接收到的字節(jié)數(shù)據(jù)轉(zhuǎn)換成String

                //打印客戶端信息和發(fā)送過來的數(shù)據(jù)
                System.out.println("address = " + address +
                        ", port = " + port + ",(Client's) msg = " + clientMsg);

                /*
                  讀取Terminal的輸入
                  next()也是阻塞的霜大,監(jiān)聽Terminal輸入(消息+回車)

                  給客戶端返回數(shù)據(jù),
                  返回的數(shù)據(jù)我們希望可以在控制面板Terminal上寫构哺,
                  寫完按Enter鍵完成
                 */
                String returnedMsg = mScanner.next();
                byte[] returnedMsgBytes = returnedMsg.getBytes();//將String轉(zhuǎn)換成byte數(shù)組

                //getSocketAddress中包含getAddress(), getPort(),即包含地址跟數(shù)組
                //下面把需要返回給客戶端的數(shù)據(jù)封裝成一個DatagramPacket
                DatagramPacket sendPacket = new DatagramPacket(returnedMsgBytes,
                        returnedMsgBytes.length, receivedPacket.getSocketAddress());
                mSocket.send(sendPacket);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new UdpServer().start();
    }
}

如此便完成了UDP Server的代碼的編寫战坤,
相對比較簡單曙强,
涉及到的API就是以上所說的DatagramSocket以及DatagramPacket
接收途茫、發(fā)送數(shù)據(jù)時候碟嘴,
都要提前封裝一個DatagramPacket對象检访,
接收時的封裝傳入的參數(shù):緩存字節(jié)數(shù)組引用及其長度掘托;
發(fā)送時的封裝傳入的參數(shù):緩存字節(jié)數(shù)組引用及其長度、封裝了客戶端(發(fā)送目的地)ip季俩、portInetAddress對象边败;
然后通過receive()send()操作即可袱衷;




創(chuàng)建UDP客戶端

  • 先創(chuàng)建java文件,調(diào)試完畢之后笑窜,再移植到Android上來致燥;

  • udp包下,創(chuàng)建一個UdpClient

package com.lwp.sample.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Scanner;

/**
 * <pre>
 *     author : 李蔚蓬(簡書_凌川江雪)
 *     time   : 2019/10/28 15:20
 *     desc   :
 * </pre>
 */
public class UdpClient {

    /**
     * 指定Server的 ip 和 port
     */
    private String mServerIp = "***";
    private int mServerPort = 7777;
    private InetAddress mServerAddress;
    /**
     * 通信用的Socket
     */
    private DatagramSocket mSocket;
    private Scanner mScanner;

    //構(gòu)造方法中初始化
    public UdpClient() {
        try {

            /*
                直接實例化一個默認的Socket對象即可排截,
                因為我們不需要像服務(wù)端那樣把別的Client接入過來嫌蚤,
                不必特別明確指定 自己的ip和port(服務(wù)程序),6习痢M阎ā!H险帧O潋稹!!;掳帷Q榔啊!
                因為這里是Client间校,是數(shù)據(jù)請求獲取方矾克,不是數(shù)據(jù)提供方,c咀恪P哺健!滓彰!

                所以只需要一個默認的Socket對象
                來進行send 和 receive 即可
             */
            mSocket = new DatagramSocket();

            mServerAddress = InetAddress.getByName(mServerIp);

            mScanner = new Scanner(System.in);
            mScanner.useDelimiter("\n");
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    public void start() {

        while (true) {

            try {
                /*
                    完成向服務(wù)端發(fā)送數(shù)據(jù)
                 */
                String clientMsg = mScanner.next();
                byte[] clientMsgBytes = clientMsg.getBytes();
            /*
               封裝數(shù)據(jù)包控妻,傳入數(shù)據(jù)數(shù)組以及服務(wù)端地址、端口號
             */
                DatagramPacket clientPacket = new DatagramPacket(clientMsgBytes,
                        clientMsgBytes.length, mServerAddress, mServerPort);
                mSocket.send(clientPacket);


                /*
                    接收服務(wù)端數(shù)據(jù)
                 */
                byte[] buf = new byte[1024];
                DatagramPacket serverMsgPacket = new DatagramPacket(buf, buf.length);
                mSocket.receive(serverMsgPacket);

                //拿到服務(wù)端地址揭绑、端口號饼暑、發(fā)送過來的數(shù)據(jù)
                InetAddress address = serverMsgPacket.getAddress();
                int port = serverMsgPacket.getPort();
                byte[] data = serverMsgPacket.getData();
                String serverMsg = new String(data, 0, data.length);//把接收到的字節(jié)數(shù)據(jù)轉(zhuǎn)換成String

                //打印服務(wù)端信息和發(fā)送過來的數(shù)據(jù)
                System.out.println("(Server's) msg = " + serverMsg);

            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    public static void main(String[] args) {
        new UdpClient().start();
    }

}

private String mServerIp = "***";這里,我把代碼的內(nèi)容略去了洗做,
這部分使用的是本機的網(wǎng)絡(luò)IPV4的ip,
查看本機ip方法傳送門彰居,
為何用的是本機ip呢诚纸,因為UdpServer中:
mInetAddress = InetAddress.getLocalHost();處,
設(shè)置的服務(wù)端ip正是本機ip


  • 開始測試

注意陈惰,
程序運行第二次的時候畦徘,
如果第一次運行沒有對鏈接進行關(guān)閉,
則第一次運行的端口號會被占用抬闯,
導(dǎo)致第二次相關(guān)程序運行時Socket對象無法實例化井辆,
以致于Socket對象空(NULL)
程序報空指針的錯誤溶握!

為了避免這種情況杯缺,
可以在不需要Server的時候,將Server程序暫停睡榆;
也可以在更改程序之后萍肆,使PC睡眠再重新打開,亦可刷新port占用胀屿;
或者直接為更改后的程序指定新的port塘揣,當然這種方法不推薦;

進入UdpServer.class宿崭,右鍵啟動main程序:

同理啟動UdpClient.class的main程序亲铡,啟動完畢彈出底部界面如下:




Client端敲一個"nihao I am Client",然后回車:




切換到UdpServer終端,可以看到接收到信息:



反復(fù)測試:




移植客戶端

  • 將UDP客戶端程序移植到Android中奖蔓;
    activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/id_btn_send"
        android:text="Send"
        android:layout_width="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/id_et_msg"
        android:layout_toLeftOf="@+id/id_btn_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <ScrollView
        android:layout_below="@+id/id_et_msg"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/id_tv_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </ScrollView>

</RelativeLayout>
  • 建一個Package名為biz赞草,用于打包業(yè)務(wù)代碼:
    • 直接copy那UdpClient.class,粘貼在biz包下锭硼,

      改名為UdpClientBiz:

編寫之:

public class UdpClientBiz {

    /**
     * 指定Server的 ip 和 port
     */
    private String mServerIp = "172.18.1.59";
    private int mServerPort = 7778;
    private InetAddress mServerAddress;

    private Handler mUIHandler = new Handler(Looper.getMainLooper());

    /**
     * 通信用的Socket
     */
    private DatagramSocket mSocket;

    //構(gòu)造方法中初始化
    public UdpClientBiz() {
        try {

            /*
                直接實例化一個默認的Socket對象即可房资,
                因為我們不需要像服務(wù)端那樣把別的Client接入過來,
                不必特別明確指定 自己的ip和port(服務(wù)程序)檀头,:湟臁!J钍肌4疃馈!@染怠Q栏巍!`推印配椭!
                因為這里是Client,是數(shù)據(jù)請求獲取方雹姊,不是數(shù)據(jù)提供方股缸,!Vǔ6匾觥!

                所以只需要一個默認的Socket對象
                來進行send 和 receive 即可
             */
            mSocket = new DatagramSocket();

            mServerAddress = InetAddress.getByName(mServerIp);

        } catch (SocketException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    /*
        需求:客戶端接收Server端返回的數(shù)據(jù)歧杏,并展示在控件上
        實現(xiàn):send 方法綁定一個接口
        ps:這里的回調(diào)機制實現(xiàn)其實還有一種寫法镰惦,
        就是另外單獨再起一個setListener方法來綁定Listener ,
        但是這樣做不太符合這里具體的場景——每個 服務(wù)端 return 回來的數(shù)據(jù)
        都是跟每個 客戶端 send 出去的數(shù)據(jù)相關(guān)聯(lián)對應(yīng)的犬绒;
        單獨使用setListener 的方式旺入,看不到這個關(guān)聯(lián)的邏輯,
        所以這里直接把Listener 作為sendMsg 的必要形參懂更,形成關(guān)聯(lián)邏輯
        以及綁定關(guān)系——必須先 sendMsg 之后才能 returnMsg(receiveMsg)
     */
    public interface onMsgReturnedListener{
        void onMsgReturned(String msg);
        /*
            Handle Exception
            如果是異步的方法調(diào)用:可以把Exception 通過 Listener 給回調(diào)出去
            如果是同步的方法調(diào)用:盡可能不要在方法中進行try catch眨业,
            最好是將其throw 出去,
            或者catch 之后 封裝下錯誤類型再將其throw 出去沮协,
            即一定要讓調(diào)用者能知道這個異常龄捡;

            這里是異步調(diào)用
         */
        void onError(Exception ex);
    }

    public void sendMsg(final String msg, final onMsgReturnedListener listener) {
        new Thread() {
            @Override
            public void run() {
                try {
                    //信息轉(zhuǎn)型
                    byte[] clientMsgBytes = msg.getBytes();
            /*
               封裝數(shù)據(jù)包,傳入數(shù)據(jù)數(shù)組以及服務(wù)端地址慷暂、端口號
             */
                    DatagramPacket clientPacket = new DatagramPacket(clientMsgBytes,
                            clientMsgBytes.length, mServerAddress, mServerPort);
                    mSocket.send(clientPacket);

             /*
                接收服務(wù)端數(shù)據(jù)
             */
                    byte[] buf = new byte[1024];
                    DatagramPacket serverMsgPacket = new DatagramPacket(buf, buf.length);
                    mSocket.receive(serverMsgPacket);

                    //拿到服務(wù)端地址聘殖、端口號晨雳、發(fā)送過來的數(shù)據(jù)
                    InetAddress address = serverMsgPacket.getAddress();
                    int port = serverMsgPacket.getPort();
                    byte[] data = serverMsgPacket.getData();
                    final String serverMsg = new String(data, 0, data.length);//把接收到的字節(jié)數(shù)據(jù)轉(zhuǎn)換成String

                    /*
                        以上是信息的發(fā)送和接收,寫在sendMsg 方法體中奸腺,名副其實
                        以下是對接收數(shù)據(jù)的處理餐禁,通過回調(diào)處理
                     */
                    //這里是子線程,
                    // 但是 Handler 已同 MainLooper 進行綁定突照,
                    // 則利用這個handle 去更新UI帮非,等同于切回主線程更新UI
                    mUIHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            //數(shù)據(jù)借助回調(diào)外傳
                            //“切回了”主線程,在調(diào)用的時候讹蘑,接收數(shù)據(jù)之后才能更新UI
                            listener.onMsgReturned(serverMsg);
                        }
                    });
                } catch (final Exception e) {

                    mUIHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            //異衬┛回調(diào)
                            listener.onError(e);
                        }
                    });

                }
            }
        }.start();

    }

    public void onDestroy() {
        if(mSocket != null){
            mSocket.close();
        }

    }
}

MainActivity.java:

public class MainActivity extends AppCompatActivity {

    private EditText mEtMsg;
    private Button mBtnSend;
    private TextView mTvContent;

    private UdpClientBiz mUdpClientBiz = new UdpClientBiz();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();
    }

    private void initViews() {
        mEtMsg = findViewById(R.id.id_et_msg);
        mBtnSend = findViewById(R.id.id_btn_send);
        mTvContent = findViewById(R.id.id_tv_content);

        mBtnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = mEtMsg.getText().toString();
                if (TextUtils.isEmpty(msg)) {
                    return;
                }

                appendMsgToContent("client:" + msg);

                //發(fā)送后清除編輯框文本
                mEtMsg.setText("");

                mUdpClientBiz.sendMsg(msg, new UdpClientBiz.onMsgReturnedListener() {
                    @Override
                    public void onMsgReturned(String msg) {

                        //更新UI
                        appendMsgToContent("server:" + msg);
                    }

                    @Override
                    public void onError(Exception ex) {
                        ex.printStackTrace();
                    }
                });
            }
        });
    }

    private void appendMsgToContent(String msg) {
        mTvContent.append(msg + "\n");
    }

    /*
        回收資源
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mUdpClientBiz.onDestroy();
    }
}

然后記得添加網(wǎng)絡(luò)權(quán)限:

<uses-permission android:name="android.permission.INTERNET"/>
  • 測試:
    啟動UdpServer:

    啟動sample模塊;
    反復(fù)測試:

本例用的服務(wù)器ip是真實的ipv4的網(wǎng)絡(luò)本機ip座慰,
所以將客戶端安裝在可以聯(lián)網(wǎng)的真機
也是可以跑的陨舱;
只是這里為了錄圖方便就跑在虛擬機調(diào)試而已。





參考自版仔,慕課網(wǎng)游盲。就業(yè)班

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蛮粮,隨后出現(xiàn)的幾起案子益缎,更是在濱河造成了極大的恐慌,老刑警劉巖然想,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件链峭,死亡現(xiàn)場離奇詭異,居然都是意外死亡又沾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進店門熙卡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杖刷,“玉大人,你說我怎么就攤上這事驳癌』迹” “怎么了?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵颓鲜,是天一觀的道長表窘。 經(jīng)常有香客問我,道長甜滨,這世上最難降的妖魔是什么乐严? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮衣摩,結(jié)果婚禮上昂验,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好既琴,可當我...
    茶點故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布占婉。 她就那樣靜靜地躺著,像睡著了一般甫恩。 火紅的嫁衣襯著肌膚如雪逆济。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天磺箕,我揣著相機與錄音奖慌,去河邊找鬼。 笑死滞磺,一個胖子當著我的面吹牛升薯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播击困,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼涎劈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了阅茶?” 一聲冷哼從身側(cè)響起蛛枚,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脸哀,沒想到半個月后蹦浦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡撞蜂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年盲镶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝌诡。...
    茶點故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡溉贿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浦旱,到底是詐尸還是另有隱情宇色,我是刑警寧澤,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布颁湖,位于F島的核電站宣蠕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏甥捺。R本人自食惡果不足惜抢蚀,卻給世界環(huán)境...
    茶點故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镰禾。 院中可真熱鬧思币,春花似錦鹿响、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至博投,卻和暖如春绸贡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背毅哗。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工听怕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虑绵。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓尿瞭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翅睛。 傳聞我的和親對象是個殘疾皇子声搁,可洞房花燭夜當晚...
    茶點故事閱讀 43,499評論 2 348