第11次文章:網路編程——聊天室構建
- 2019 年 10 月 8 日
- 筆記
這周的內容還是蠻有意思的!構建一個聊天室,如果我們20年前掌握了這篇文章的內容,那我們就離馬化騰不遠了!哈哈哈!
1、網路:將不同區域的電腦連接起來,比如:區域網、城域網、互聯網。
2、地址:IP地址 確定網路上的一個絕對地址類似於:房子。
3、埠號:區分電腦不同軟體,類似於房子的房門。埠號長度為2個位元組,範圍為:0---65535,共有65536個。
tips:在使用戶埠號的時候,在同一個協議下,埠號不能重複,如果在不同協議下,則埠號可以重複。1024以下的埠號不要使用,主要是留給設備服務商使用的固定埠號
4、資源定位:
URL:統一資源定位符
URI:統一資源
5、數據的傳輸:
TCP協議:類似於打電話,有三次握手機制,面向連接,安全可靠,效率低下。
UDP協議:類似於發簡訊,非面向連接,效率高,但是不可靠,可能存在資訊丟失的情況。
二、網路編程中的一些基本類
1、地址及埠:
(1)InetAddress:封裝電腦的ip地址和DNS,沒有埠
方法:
getLocalHost():獲取本地地址
getHostName():返回域名
getHostAddress():返回IP地址
getByName():通過域名或者IP來獲取地址
(2)InetSocketAddress:在InetAddress基礎上+埠
創建對象:
InetSocketAddress(String hostname, int port)
InetSocketAddress(InetAddress addr, int port)
方法:
getHostName():獲取域名
getPort():獲取埠號
getAddress():獲取InetAddress對象
2、URL:
四部分組成: 協議 存放資源的主機域名 埠 資源文件名(/)
(1)創建
URL(String spec):絕對路徑構建
URL(URL context, String spec):相對路徑構建
(2)方法
package com.peng.net.url; import java.net.MalformedURLException; import java.net.URL; public class URLDemo01 { public static void main(String[] args) throws MalformedURLException { //絕對路徑構建 URL url = new URL("http://www.baidu.com:80/index.html#aa?uname=peng"); System.out.println("協議:"+url.getProtocol()); System.out.println("域名:"+url.getHost()); System.out.println("埠:"+url.getPort()); System.out.println("資源:"+url.getFile()); System.out.println("相對路徑:"+url.getPath()); System.out.println("錨點:"+url.getRef()); System.out.println("參數:"+url.getQuery());//如果存在錨點,則將參數視為錨點的一部分,返回null;如果不存在錨點,則返回參數 //相對路徑 url = new URL("http://www.baidu.com/a/"); url = new URL(url,"b/c.txt"); System.out.println(url.toString()); } }三、UDP編程,基本概念:
UDP:以數據為中心,非面向連接,不安全,數據可能丟失,效率高。
1、客戶端
1)創建客戶端 DatagramSocket 類 +指定發送埠
2)編輯數據 位元組數組
3)打包 DatagramPacket + 伺服器地址 + 指定的接收埠
4)發送數據
5)釋放資源
2、伺服器端
1)創建伺服器端 DatagramSocket 類 + 指定接收埠
2)創建接收容器 位元組數組
3)打包封裝 DatagramPacket
4)包 接收數據
5)分析
6)釋放資源
由於UDP協議編程是非面向連接的,TCP協議編程面向連接,相比之下TCP更加複雜,所以此處不放入UDP編程進行講解,我們結合後面的TCP編程進行解析UDP編程細節。
四、基於TCP編程:
面向連接 安全可靠 效率低,類似於打電話
1、面向連接:請求-響應 Request--Response
2、Socket編程
1)、伺服器:SeverSocket
2)、客戶端:Socket
基本的TCP相關協議我們在下面一個實例中進行講解——聊天室創建,其中包含有群聊和私聊功能。
基本的通訊思路如下圖所示:

在客戶端首先和伺服器端建立連接通道,也就是socket,然後在傳輸通道中進行數據的傳輸,每一個通道內的藍色箭頭,代表著數據的輸入和輸出流。並且在數據的發送和接收過程中,可以同時進行,不會受到彼此的影響。在同一個聊天室中,具有多個客戶端,他們需要同時連接在我們的伺服器端上,因此我們在設計的過程中需要進行多執行緒的應用。
第一步:我們首先對客戶端的接收數據進行封裝,創建接收通道。
package com.peng.net.tcp.chat.demo04; /** * 從伺服器接收數據 */ import java.io.DataInputStream; import java.io.IOException; import java.net.Socket; public class Receive implements Runnable{ //管道輸入流 private DataInputStream dis ; //執行緒標識符 private boolean isRunning = true; //構造器 public Receive() { } public Receive(Socket client) { try { //獲取客戶端與伺服器之間的傳輸管道 dis = new DataInputStream(client.getInputStream()); } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis); } } /** * 獲取從伺服器發送到客戶端的數據 * @return */ public String receive() { String msg = ""; try { msg = dis.readUTF();//從管道輸入流中讀取發送過來的數據內容 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis); } return msg; } @Override public void run() { //執行緒體 while(isRunning) { System.out.println(receive());//在客戶端的控制台上列印接收到的數據內容 } } }解析:在接收數據的過程中,我們主要思路是,在構造器中對輸入流進行初始化操作,應用「DataInputStream」輸入流,然後加入一個接收方法,將管道中伺服器傳回來的數據進行讀取,最後在執行緒體中,將讀取到的內容傳輸到客戶端的介面上。
由於我們在多執行緒的使用中,頻繁使用關閉輸入輸出流的關閉操作,所以我們將輸入輸出流的關閉操作封裝成為一個單獨的類,這樣便於我們後期的調用和處理。
package com.peng.net.tcp.chat.demo04; import java.io.Closeable; import java.io.IOException; /** * 關閉流 */ public class CloseUtil { public static void closeAll(Closeable... io) { for (Closeable tem:io) { try { if(null !=tem ) { tem.close(); } } catch (IOException e) { e.printStackTrace(); } } } }tips:在對其進行封裝的過程中,注意我們使用到了程式碼「Closeable...」,其中的運算符「...」相當於數組「[]」。
第二步:我們對客戶端的發送操作進行一個封裝操作。
package com.peng.net.tcp.chat.demo04; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; /** * 發送數據執行緒 */ public class Send implements Runnable{ //控制台輸入流 private BufferedReader console ; //管道輸出流 private DataOutputStream dos ; //客戶端的名稱 private String name; //執行緒標識符 private boolean isRunning = true; //構造器:初始化輸入輸出流 public Send() { console = new BufferedReader(new InputStreamReader(System.in)); } public Send(Socket client,String name) { this(); try { //初始化客戶端向伺服器端發送資訊的管道 dos = new DataOutputStream(client.getOutputStream()); this.name = name; this.send(this.name);//在接收到名稱時,就將客戶端的名稱發送出去 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } } /** * 從控制台獲取數據 * @return */ public String getMsgFromConsole() { try { return console.readLine();//獲取控制台輸入的數據 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } return ""; } /** * 將客戶端的數據發送給伺服器 * @param info */ public void send(String info) { try { if(null != info && !info.equals("")) { dos.writeUTF(info);//寫出數據資訊 dos.flush();//強制刷新 } } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dos,console); } } @Override public void run() { //執行緒體 while(isRunning) { send(getMsgFromConsole()); } } }解析:在進行發送操作的時候,我們需要有兩個流操作,一個是輸入流,主要負責從控制台上接收客戶端輸入的數據,另一個是輸出流,主要負責將從客戶端上獲取到的資訊發送到伺服器進行操作。所以我們為了降低方法之間的耦合性,使用了兩個方法,分別封裝其功能。在最後的執行緒體中,我們將接收到的數據直接發送給客戶端。注意,我們在構造器中發送了一個名稱給客戶端,這一點在我們創建客戶端的程式碼中會進行解釋。
第三步:創建客戶端
package com.peng.net.tcp.chat.demo04; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; /** * 創建客戶端:發送數據+接收數據 * 寫出數據:輸出流 * 讀取數據:輸入流 * 同時需要將輸入流和輸出流分別封裝起來,彼此獨立,相互獨立處理 * 加入客戶端名稱 */ public class Client { public static void main(String[] args) throws IOException { System.out.println("請輸入名稱:"); //從控制台輸入客戶端名稱 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//新建輸入流 String name = br.readLine();//獲取客戶端名稱 if("" == name) {//如果名字為空,則退出 return; } //創建客戶端,與伺服器進行連接,並指定埠號 Socket client = new Socket("localhost",9999); new Thread(new Send(client,name)).start();//發送路徑 new Thread(new Receive(client)).start();//接受路徑 } }解析:在創建客戶端的時候,我們首先需要獲取每一個客戶端的名稱,在獲取到名稱之後,我們立刻將客戶端的名稱發送給伺服器後,伺服器會進行一定的回饋,返回給客戶端的消息為:「歡迎加入聊天室」,然後在其他客戶端的介面上,輸出「XXX加入了聊天室」。
tips:在UDP協議中,客戶端發送數據的時候,需要指定客戶端發送埠,以及伺服器的接收埠,這一點與TCP協議編程中有所不同。在TCP編程中,客戶端不需要指定對應的發送埠,系統會自動分配給客戶端埠,但是並非TCP不要需要埠,只是開發者在編程的時候可以省略而已。
第四步:創建伺服器
package com.peng.net.tcp.chat.demo04; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; /** * 創建伺服器 加入多執行緒 * 實現多個不同的客戶端可以同時進行發送接收數據 */ public class Server { //存儲所有客戶端與伺服器建立的管道 private List<MyChannel> all = new ArrayList<MyChannel>(); public static void main(String[] args) throws IOException { new Server().start(); } public void start() throws IOException { //創建伺服器,指定埠號 ServerSocket server = new ServerSocket(9999); while(true) { Socket client = server.accept();//接收客戶端請求,並與客戶端建立連接 MyChannel channel = new MyChannel(client); all.add(channel);//向容器中加入客戶端通道,便於統一管理 new Thread(channel).start();//一條道路 } } /** * 定義匿名內部類,便於調用類的屬性,此匿名類相當於客戶端和伺服器之間建立的道路 * 一個客戶,一條道路 * 1、輸入流:接收數據 * 2、輸出流:發送數據 */ private class MyChannel implements Runnable{ //輸入流:接收數據 private DataInputStream dis; //輸出流:發送數據 private DataOutputStream dos; //客戶端的名稱 private String name; //執行緒運行標識符 private boolean isRunning = true; //構造器 public MyChannel(Socket client) { try { dis = new DataInputStream(client.getInputStream()); dos = new DataOutputStream(client.getOutputStream()); this.name = dis.readUTF();//獲取客戶端名稱 this.send("歡迎加入聊天室");//向本客戶端發送此資訊 this.sendAll(this.name+"加入了聊天室",true);//向其他的客戶端發送該資訊 } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } } /** * 接收從客戶端發送過來的資訊 * @return */ public String receive () { String msg = ""; try { msg = dis.readUTF();//獲取讀入的資訊 send("you say :"+msg); } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } return msg; } /** * 向本客戶端發送相關資訊 * @param msg */ public void send(String msg) { try { if(null != msg && !msg.equals("")) { dos.writeUTF(msg); dos.flush(); }else { return; } } catch (IOException e) { isRunning = false; CloseUtil.closeAll(dis,dos); all.remove(this);//移除通道自身 } } /** * 向除本客戶端以外的其他客戶端發送資訊 * 根據發送的消息區分是私聊還是群聊 * 在群發的消息中,使用flag區分該消息是伺服器的系統消息,還是用戶群聊的資訊 * @param msg * @param flag */ public void sendAll(String msg,boolean flag) { //約定,如果發送的資訊中包含有「@name:.....」,則將name取出,然後與該用戶進行私聊 if(msg.startsWith("@") && msg.indexOf(":")>-1) {//私聊 String name = msg.substring(1,msg.indexOf(":"));//獲取私聊對象的名稱 String contents = msg.substring(msg.indexOf(":")+1);//獲取私聊的內容 for(MyChannel other:all) {//遍歷所有客戶端 if(other.name.equals(name)) {//存在將要私聊的對象 other.send(this.name+"對您悄悄說:"+contents); return; } } this.send("當前聊天室中不存在此用戶"); }else {//群聊 if(flag) {//屬於系統消息 for(MyChannel other:all) { if(this == other) {//跳過本客戶端自身 continue; } //將本客戶端發送的數據,發送給其他已經加入聊天的客戶 other.send("系統消息:"+msg); } }else { for(MyChannel other:all) { if(this == other) {//跳過本客戶端自身 continue; } //將本客戶端發送的數據,發送給其他已經加入聊天的客戶 other.send(this.name+"對大家說:"+msg); } } } } @Override public void run() { //執行緒體 while(isRunning) { sendAll(receive(),false); } } } }解析:
1、正如我們對聊天室的功能分析上,聊天室應該具有群聊和私聊的基本功能。所以我們根據客戶端發送的消息進行區分是私聊還是群聊,具體的規則為:伺服器獲取到客戶端發送進來的數據,然後如果該消息以「@XXX:」開頭,則獲取「@」和「:」中間的名稱"XXX",然後在所有客戶端中進行搜索名稱為「XXX」的客戶端,伺服器將該消息僅僅轉發給客戶」XXX「。
2、在我們管理聊天室中的所有客戶的時候,我們使用了容器List進行統一管理。但是這裡在導入包的時候,一定要注意,此處導入的是容器類包java.util.List。在我們使用自動導包過程時,eclipse給我們的提示中,還有一個是java.awt.List,這個包是java中GUI介面操作的工具類包,千萬要注意此處的導包,一旦導錯之後,很難檢查出錯誤。
tips:查看源碼,可以對比出兩個包繼承關係以及實現介面之間的差別,進入源碼中查詢可以看出:
java.util.List中繼承關係為:interface List<E> extends Collection<E>,主要是實現相應的容器類;
而java.awt.List繼承和實現關係為:List extends Component implements ItemSelectable, Accessible,主要是實現GUI圖形介面的工具類
第五步:運行查看一下相關的結果
a客戶端控制台資訊:

b客戶端控制台資訊:

c客戶端控制台資訊:

解析:由於我們使用的是tcp協議,需要客戶端先建立連接之後,才可以進行相互通訊傳輸數據。所以在測試的時候,需要我們首先需要運行伺服器,使伺服器處於就緒狀態,隨時接受來自客戶端的請求,然後再創建客戶端進行操作,否則會報錯。我們在測試的時候,創建了3個客戶端,分別是「a」、「b」、「c"。在測試的時候,我們使用a客戶端給b發送了hello,然後可以在b客戶端看到a發送過來的私聊資訊,而c客戶端介面上沒有出現這條資訊,所以完成了私發消息的功能。然後使用c客戶端發送了資訊a beautiful world,該資訊屬於群發資訊,所以出現在了a和b客戶端的窗口。然後在c窗口中,對一個不存在的對象d進行發送資訊,可以看到伺服器返回的資訊:當前聊天室中不存在該用戶。系統具有一定的容錯率。