更新時間:2022-09-16 來源:黑馬程序員 瀏覽量:
大家好,今天為大家?guī)砹艘粋€非常有意思的小程序——UDP實現(xiàn)的群聊聊天室。這個程序使用的UDP協(xié)議,并使用DatagramSocket的子類MulticastSocket實現(xiàn)組播,可以部署在一個局域網(wǎng)內(nèi)的多臺電腦上,并可以實現(xiàn)文字群聊。
本文將會按照以下幾個小節(jié)講解:
1). 組播的概念:**這個小節(jié)我們將講解什么是:單播、廣播、組播。
2). MulticastSocket類的使用:**這個小節(jié)我們將講解MulticastSocket類的基本使用,并實現(xiàn)控制臺的信息收發(fā)。
3). 基于Swing和MulticastSocket實現(xiàn)的UDP群聊聊天室:**這個小節(jié)我們將制作一個界面,并結(jié)合MulticastSocket類實現(xiàn)一個完整的UDP群聊聊天室。
4). 結(jié)束語:
網(wǎng)絡(luò)數(shù)據(jù)傳播按照接收者的數(shù)量,可分為以下3種方式:
單播是指實現(xiàn)“點對點”的通信,發(fā)送者發(fā)送數(shù)據(jù)要發(fā)送給網(wǎng)絡(luò)上的唯一的一臺電腦,指定一個接收者。像TCP協(xié)議和UDP協(xié)議都能實現(xiàn)點對點通信。
發(fā)送者發(fā)送的數(shù)據(jù)可以被某個接收范圍內(nèi)所有的接收者接收。它類似于廣播電臺,向某個范圍內(nèi)的所有用戶發(fā)送廣播信號,接收人打開廣播就可以聽到,關(guān)閉廣播設(shè)備就停止收聽。由于廣播會大大增加網(wǎng)絡(luò)數(shù)據(jù)流量,所以通常情況下一些網(wǎng)絡(luò)路由器會禁止廣播數(shù)據(jù),尤其是一些占用網(wǎng)絡(luò)資源比較大的視頻數(shù)據(jù)等。
組播是指發(fā)送的數(shù)據(jù)可以被指定的一組用戶接收。組播的范圍沒有廣播那么廣,任何的一臺電腦都可以隨時加入某一個組接收組播數(shù)據(jù)。若要使用組播,則需要讓一個數(shù)據(jù)報標(biāo)有一組目標(biāo)主機(jī)地址,當(dāng)數(shù)據(jù)報發(fā)出后,整個組的所有主機(jī)都能收到該數(shù)據(jù)報。IP協(xié)議為組播提供了這批特殊的IP地址,這些IP地址的范圍是224.0.0.0至239.255.255.255。在Java類庫中,DatagramSocket有一個子類:MulticastSocket,它具有組播的功能,它可以與DatagramPackage結(jié)合使用,用于發(fā)送和接收組播包。
Java類庫中MulticastSocket類可以實現(xiàn)組播功能,它是DatagramSocket的子類:
通過API文檔我們可以看到它有三個構(gòu)造方法:
1. MulticastSocket() 創(chuàng)建一個多播套接字。(使用隨機(jī)端口,如果只發(fā)送,可以使用這個構(gòu)造方法) 2. MulticastSocket(int port) 創(chuàng)建一個多播套接字并將其綁定到一個特定的端口。(如果需要發(fā)送和接收,需要使用這個構(gòu)造方法) 3. MulticastSocket(SocketAddress bindaddr) 創(chuàng)建一個多播套接字綁定到指定的套接字地址。
以下是幾個比較重要的成員方法:
1.public void joinGroup(InetAddress mcastaddr):將該MulticastSocket加入指定的多點廣播地址。 2.public void leaveGroup(InetAddress mcastaddr讓該MulticastSocket離開指定的多點廣播地址。 3.public void setInterface(InetAddress inf):如果當(dāng)前系統(tǒng)有多個網(wǎng)絡(luò)接口,可以使用次方法指定一個網(wǎng)絡(luò)接口。 4.public InetAddress getInterface():獲取當(dāng)前的網(wǎng)絡(luò)接口。 5.public void setTimeToLive(int ttl):該參數(shù)設(shè)置數(shù)據(jù)報最多可以跨過多少個網(wǎng)絡(luò),當(dāng)ttl為0時,指定數(shù)據(jù)報應(yīng)停留在本地主機(jī);當(dāng)ttl的值為1時,指定數(shù)據(jù)報發(fā)送到本地局域網(wǎng);當(dāng)ttl的值為32時,意味著只能發(fā)送到本站點的網(wǎng)絡(luò)上;當(dāng)ttl為64時,意味著數(shù)據(jù)報應(yīng)保留在本地區(qū);當(dāng)ttl的值為128時,意味著數(shù)據(jù)報應(yīng)保留在本大洲;當(dāng)ttl為255時,意味著數(shù)據(jù)報可發(fā)送到所有地方;默認(rèn)情況下,該ttl的值為1。
接下來我們寫一個小例子來看一下MulticastSocket的使用方式。這個程序?qū)瑑蓚€線程:1. 接收線程,主要用于接收信息;2. 主線程,主要用于發(fā)送信息。將這個程序部署到局域網(wǎng)上的幾臺電腦上,全部啟動,就可以實現(xiàn)多臺電腦的組播了,而且每臺主機(jī)都可以發(fā)出信息,其它主機(jī)則會收到這條信息。
package com.heima.se.chat; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.Date; import java.util.Scanner; public class MulticastSocketDemo { public static void main(String[] args) throws IOException { //創(chuàng)建MuticastSocket對象,并監(jiān)聽端口55555 MulticastSocket socket = new MulticastSocket(55555); //加入組:235.235.235.235 socket.joinGroup(InetAddress.getByName("235.235.235.235")); //啟動線程-此線程用于接收數(shù)據(jù)報 new Thread(()->{ byte[] bytes = new byte[1024]; DatagramPacket packet = new DatagramPacket(bytes, bytes.length); while (true) { try { socket.receive(packet); System.out.println(new String(packet.getData(), 0, packet.getLength())); } catch (IOException e) { e.printStackTrace(); } } }).start(); //獲取本機(jī)IP String localIp = InetAddress.getLocalHost().getHostAddress(); //創(chuàng)建一個Scanner對象,用于接收控制臺數(shù)據(jù) Scanner sc = new Scanner(System.in); while (true) { System.out.println("【請輸入信息】"); String msg = sc.next(); // 獲取當(dāng)前時間格式化字符串,把IP、時間,以及要發(fā)送的文本連接在一起 String time = String.format(" <====> %tF %<tT", new Date()); msg = localIp + time + "\n" + msg + "\n\n"; //發(fā)送數(shù)據(jù)報 socket.send(new DatagramPacket(msg.getBytes(), msg.getBytes().length, InetAddress.getByName("235.235.235.235"), 55555)); } } }
通過上面的程序,我們發(fā)現(xiàn),MulticastSocket類的使用和DatagramSocket類基本相同,只是多了一步加入組:joinGroup(),所有加入這個組的主機(jī)都將會收到信息。
接下來我們使用Swing為這個程序制作一個界面,讓用戶操作起來更加方便。
這個程序我們制作了兩個類:
1). ChatFrame:這個類繼承自JFrame,實現(xiàn)了界面的顯示、布局等相關(guān)功能。
2). SocketChat:這個類繼承自ChatFrame,加入了MulticastSocket的連接、信息發(fā)送和接收。
package com.heima.se.chat; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.InetAddress; public abstract class ChatFrame extends JFrame { private JTextArea receiveArea = new JTextArea();//接收文本框,用來顯示服務(wù)器發(fā)送過來的文本 private JTextArea sendArea = new JTextArea();//發(fā)送文本框,用來顯示當(dāng)前用戶要發(fā)送的文本 private JButton sendBtn = new JButton("SEND");//發(fā)送按鍵 public ChatFrame() { this.initFrame();//初始化窗口 this.initComponent();//初始化組件 this.initListener();//初始化監(jiān)聽器 this.receive();//開啟監(jiān)聽服務(wù)器線程,把接收到的文本顯示在receiveArea中 } // 初始化監(jiān)聽器 private void initListener() { // 給發(fā)送按鍵添加監(jiān)聽器,當(dāng)被點擊時調(diào)用send()方法 sendBtn.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { send(); } }); // 給發(fā)送文本框添加鍵盤監(jiān)聽器,當(dāng)按下Ctrl+ENTER時調(diào)用send()方法 sendArea.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { if(e.isControlDown()) { if(e.getKeyCode() == KeyEvent.VK_ENTER) { send(); } } } }); } // 子類需要重寫本方法 // 在本方法中使用socket實現(xiàn)消息發(fā)送 public abstract void sendText(String text); // 子類需要重寫本方法 // 在本方法中啟動監(jiān)聽服務(wù)器線程,調(diào)用本類receiveText(String)把接收到的文本顯示出來 public abstract void receive(); // 本方法用來發(fā)送文本 public void send() { // 如果發(fā)送文本框中沒有文本,彈出警告對話框 if(sendArea.getText().equals("")) { javax.swing.JOptionPane.showMessageDialog(this, "空文本不能發(fā)送!"); sendArea.requestFocus();// 把光標(biāo)歸還給發(fā)送文本框 return; } // 調(diào)用子類的方法完成文本發(fā)送 sendText(sendArea.getText()); // 把發(fā)送文本框內(nèi)容清空 sendArea.setText(null); } // 本方法完成接收服務(wù)器消息的后續(xù)工作-在文本框中顯示服務(wù)器消息,子類的receive()方法在接收服務(wù)器消息后可以調(diào)用本方法 public void receiveText(String text) { receiveArea.append(text);//把接收到的消息添加到文本框中 // 設(shè)置光標(biāo)位置到最后,如果不設(shè)置滾動條不動 receiveArea.setCaretPosition(receiveArea.getText().length()); } // 初始化組件 private void initComponent() { // 使用接收文本框創(chuàng)建滾動窗口(把文本框添加到了滾動窗口中),總是顯示縱向滾動條,永不顯示橫向滾動條 JScrollPane sp1 = new JScrollPane(receiveArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); // 設(shè)置滾動窗口大小、位置、無邊框;并把滾動窗口添加到主窗口中 sp1.setSize(606, 350); sp1.setLocation(14, 20); sp1.setBorder(null); this.add(sp1); // 設(shè)置接收文本框背景色、不可編輯、自動換行 receiveArea.setBackground(new Color(238, 238, 238)); receiveArea.setEditable(false); receiveArea.setLineWrap(true); // 創(chuàng)建發(fā)送文本框的滾動窗口,設(shè)置自動換行、大小、位置,然后添加到主窗口中 JScrollPane sp2 = new JScrollPane(sendArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); sendArea.setLineWrap(true); sp2.setSize(606, 145); sp2.setLocation(14, 400); this.add(sp2); // 設(shè)置發(fā)送按鍵的大小、位置,并添加到主窗口中 sendBtn.setSize(68, 21); sendBtn.setLocation(553, 560); this.add(sendBtn); // 設(shè)置主窗口的標(biāo)題為當(dāng)前IP地址 try { this.setTitle(InetAddress.getLocalHost().getHostAddress()); } catch (Exception e) { throw new RuntimeException(e); } } // 初始化主窗口 private void initFrame() { // 設(shè)置主窗口的大小、布局管理器為空、背景色、位置、大小不可改變 this.setSize(640, 620); this.setLayout(null); this.setBackground(new Color(246, 246, 247)); this.setLocation(350, 50); this.setResizable(false); // 設(shè)置主窗口的“X”按鈕點擊后結(jié)束程序 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } // 顯示主窗口方法 public void setVisible(boolean b) { super.setVisible(b);//調(diào)用父類的顯示方法 sendArea.requestFocus();//讓發(fā)送文本框得到焦點 } }
這個類中定義了很多抽象方法,這些抽象方法由子類實現(xiàn)。
package com.heima.se.chat; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.Date; /** * 本類繼承了ChatFrame,ChatFrame實現(xiàn)了GUI顯示 * 本類負(fù)責(zé)使用MulticastSocket完成群聊的發(fā)送消息與接收消息 */ public class SocketChat extends ChatFrame { private MulticastSocket socket;//群組Socket public SocketChat() throws IOException { socket = new MulticastSocket(54321);//創(chuàng)建群組Socket,綁定54321端口 //加入虛擬IP:235.235.235.235指定的群組中。虛擬IP范圍是:224.0.0.1 和 239.255.255.255 //加入群組后,就可以接收群組的消息,也可以向群組發(fā)送消息了 socket.joinGroup(InetAddress.getByName("235.235.235.235")); } // 發(fā)送消息方法 public void sendText(String text) { try { // 獲取IP地址 String ip = InetAddress.getLocalHost().getHostAddress(); // 獲取當(dāng)前時間格式化字符串 String time = String.format(" <====> %tF %<tT", new Date()); // 把IP、時間,以及要發(fā)送的文本連接在一起 text = ip + time + "\n" + text + "\n\n"; // 把文本轉(zhuǎn)換成字節(jié)數(shù)組 byte[] buff = text.getBytes(); // 使用socket向群組發(fā)送,socket的send()方法需要兩個參數(shù):DatagramPacket、端口號 // DatagramPacket表示數(shù)據(jù)包,創(chuàng)建它需要三個參數(shù):數(shù)據(jù)包的內(nèi)容、數(shù)據(jù)包的字節(jié)數(shù)、要發(fā)送的IP地址 socket.send(new DatagramPacket(buff, buff.length, InetAddress.getByName("235.235.235.235"), 54321)); } catch(Exception e) { e.printStackTrace(); } } // 本方法用來接收群組發(fā)送過來的消息 public void receive() { // 創(chuàng)建監(jiān)聽群組消息的線程,并啟動它 new Thread() { public void run() { // 循環(huán)監(jiān)聽 while(true) { try { // 創(chuàng)建數(shù)據(jù)包的字節(jié)數(shù)組,大小為1KB byte[] buff = new byte[1024]; // 創(chuàng)建數(shù)據(jù)包 DatagramPacket dp = new DatagramPacket(buff, buff.length); // 接收群組發(fā)送過來的消息到數(shù)據(jù)包中 // 本方法會阻塞當(dāng)前線程,直到接收到消息為止 socket.receive(dp); // 把接收到的消息轉(zhuǎn)換成字符串 String text = new String(dp.getData(), 0, dp.getLength()); // 調(diào)用父類的方法完成顯示 receiveText(text); } catch(Exception e) {} } } }.start(); } public static void main(String[] args) throws IOException { SocketChat sc = new SocketChat(); sc.setVisible(true); } }
這個類使用MulticastSocket,使用端口:54321,組播地址:235.235.235.235。當(dāng)用戶在界面按下send按鈕時,會觸發(fā)sendText()方法發(fā)送數(shù)據(jù);receive()方法用于使用線程接收數(shù)據(jù),它是在父類的構(gòu)造方法中被觸發(fā)啟動,啟動后,使用無限循環(huán)進(jìn)行信息的接收。
這篇文章我們使用MulticastSocket類實現(xiàn)了組播功能,并使用Swing和MulticastSocket制作了一個基于UDP的群聊聊天室,希望大家能夠通過本篇文章了解MulticastSocket類的使用,有興趣的朋友可以基于這個程序,為它添加更多的功能,例如:文件的發(fā)送、接收;表情圖片的發(fā)送、接收等等。后面我還會為大家?guī)砀喔鼘嵱玫某绦?,期待大家來圍觀哦!謝謝大家!!