用Java實現基於Socket的網絡編程

前言

這幾天做計算機網絡和數據庫的課設,有一段時間沒寫博客了。今天寫一篇用Java實現的聊天室,是之前做的計算機網絡的作業,使用TCP協議,可以進行一對一、一對多的聊天,不過比較簡陋,沒有用戶界面,輸入輸出都是在控制檯。

什麼是Socket

既然需要使用到Socket,那麼首先得明白Socket是個什麼東西。我們先扯遠一點說,從計算機網絡體系結構開始。
計算機網絡體系結構
我們都知道大名鼎鼎的OSI七層協議體系並不實用,真正被廣泛使用的是TCP/IP的四層協議,而五層協議是OSI和TCP/IP的綜合體,只是爲方便介紹計算機網絡的原理才使用的。不管是幾層的協議,在經過一通操作到達運輸層時,數據已經可以從源主機傳到目的主機了。

但是實際上,真正需要進行通信的是兩臺主機中的某兩個進程。現在雖然已經將數據送到了目標主機,卻無法確定需要交付給主機內的哪個進程。就像是信已經寄到小區了,卻不知道送到哪一家。這也就是運輸層協議要搞定的問題——怎樣確定是哪個進程、以怎樣的方式來傳輸(TCP/UDP)。

爲了解決第一個問題,在運輸層中使用協議端口號來標誌本計算機中各個進程同運輸層交互時的接口。通俗地說,就是給每個家(進程)分配一個門牌號(端口號)。現在通過端口號就可以找到對應進程了。這說明,當我們需要給某臺計算機中的某個進程發送數據時,除了要知道對方的IP地址,還需要知道對方的端口號。
運輸層的功能

現在再問:什麼是Socket?在課本的定義中是:端口號拼接到IP地址即構成了套接字(Socket)。Socket的表示方法就是,在點分十進制的IP地址後面寫上端口號,例如(127.0.0.1:8888)。

當然,Socket並不是只有這一個意思。現在我們說“利用Socket實現網絡編程”,不是說用(127.0.0.1:8888)這麼一串數字就可以了,而是在說使用系統提供的 socket API來進行網絡編程。這個socket API是應用層與TCP/IP協議族通信時的中間軟件抽象層。它是一組接口,把TCP/TP協議族隱藏在接口之後,對於我們用戶而言,只需要按需求去調用接口就行了。
Socket抽象層所在的位置

爲什麼要使用Socket

我們先看沒有Socket時的狀態
各協議所處層次
對比上一張圖,可以發現在沒有Socket時,用戶進程想使用不同的協議來通信,就要用到不同的接口,自己來實現各個協議的細節,這樣無疑很麻煩。當有了Socket之後,我們可以不用管這些細節,直接調用Socket提供的接口就行了。

這有點像Java給我們提供了一個sort方法進行排序,我們就可以不用管什麼快速排序、插入排序、歸併排序等等等這些怎麼實現,也不用管什麼時候用哪個排序更高效,只需要調用方法就好了。

所以說,使用Socket是爲了編程更方便。

Socket的系統調用(以C/S模式使用TCP協議爲例)

C/S模式有客戶端和服務器端之分,不同端所調用的方法是不一樣的,大致流程如下:
Socket的系統調用

服務器端

  • 使用socket()函數創建一個socket描述符,用於唯一標識一個socket
    (此時,這個socket的IP地址和端口號都是空的)
  • 使用bind()函數指明該socket的本地地址(包括IP地址和端口號)
  • 使用listen()函數監聽是否有客戶端傳來的connect()請求
  • 服務器監聽到客戶端傳來的connect()請求時,調用accept()函數接收請求
  • 調用網絡I/O來進行讀寫操作,用於讀取、發送通信數據。網絡I/O有很多組:read()/write()、
    recv()/send()、readv()/writev()、recvmsg()/sendmsg()、recvfrom()/sendto(),不同的在實現上有差別,任選一組即可。
  • 當操作完成後,使用close()函數來關閉這個socket

客戶端

  • 使用socket()函數創建一個socket描述符,用於唯一標識一個socket
  • 客戶端可以不調用bind()函數,這時候操作系統會自動分配一個動態端口號,通信結束後收回
  • 使用connect()函數,指明服務器的socket,然後發起與服務器的連接請求
    (所謂三次握手就是在這個過程中進行的)
  • 調用網絡I/O來進行讀寫操作,用於讀取、發送通信數據
  • 當操作完成後,使用close()函數來關閉這個socket
    (所謂四次握手就是在這個過程中進行的)

Java中Socket的使用

在JAVA中,有基於TCP協議的服務器端基礎類ServerSocket和客戶端基礎類Socket。也有基於UDP協議的DatagramSocket類。這裏主要講TCP協議。

ServerSocket類是java執行服務器端操作的基礎類,該類運行於服務器,監聽入棧TCP連接,每個socket服務器監聽服務器的某個端口,當遠程主機的客戶端嘗試連接此端口時,服務器就被喚醒,並返回一個表示兩臺主機之間socket的正常的Socket對象。

Socket類是java執行客戶端TCP操作的基礎類,這個類本身使用代碼通過主機操作系統的本地TCP棧進行通信。Socket類的方法會建立和銷燬連接,設置各種Socket選項。

Java中的Socket調用
之前說了一大堆,實際上要使用JAVA進行基本的網絡通信很簡單,只需要調用幾個方法而已。可以發現,在整個過程中關於Socket的操作代碼非常少,就幾行。剩下的都是字節流、字符流的操作和業務邏輯的處理。

程序實現思路

程序使用的是TCP協議,TCP是面向連接的協議,也就是說,在通信開始之前,服務器與客戶端需要先建立連接,通信過程中需要一直保持連接。如果說一對一的聊天指的是服務器與客戶端的聊天,那無疑已經實現了。但是現在我希望客戶端與客戶端之間進行一對一的聊天,甚至一對多的聊天,又該怎麼辦呢?TCP不同於UDP,UDP支持一對一、一對多、多對一與多對多的交互通信,但TCP只能一對一;而客戶端與客戶端之間再單獨建立連接顯然很不可取。

好消息是,有服務器端的存在,可以讓服務器來執行轉發的任務:所有的客戶端都向服務器端發送數據,服務器端解析到數據的目標客戶端,然後再轉發給這個目標客戶端,無論目標端是一個還是多個。

這樣,我們可以通過服務器來實現一對一、一對多的通信了。但新的問題是:客戶端向服務器端發送的都是數據流,服務器怎麼知道哪個是對全體的消息,哪個是私聊的消息,消息正文是什麼,目標客戶端又是誰呢?

所以說,我們還需要自己規定一套協議,來控制不同類型的格式。例如,在消息正文之前添加一段“+public+”來表示這是對全體的消息,服務器端進行解析之後,對不同類型的消息執行不同的操作。

現在,主要問題都解決了,只剩下具體的實現。下面是程序的流程圖
流程圖-服務器端
流程圖-客戶端
需要注意的是,由於服務器端需要一直處於阻塞狀態等待客戶端連接,每當有客戶端來連接時,只能開啓子線程來處理與這個客戶端的連接(每來一個客戶端,就會開啓一個子線程)。而用戶端需要不斷從終端(也就是控制檯)接收輸入,然後發送給服務器端,也需要一直處於阻塞狀態;所以也要開啓一個子線程來處理服務器端發送的數據(當連接成功後,就開啓這個子線程)

源代碼

由於類有點多,就不一個個複製粘貼了,上鍊接
CSDN下載
騰訊微雲
運行截圖:
服務器端
客戶端-1
客戶端-2
客戶端-3

參考文獻

socket工作原理深入分析
socket通訊原理及例程(一看就懂)
《計算機網絡(第七版)謝希仁著》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章