前言:
如今,網絡編程已然成為了一個后端開發工程師需要具備的核心技能之一。因此,該博客力求提供最簡單、通俗的描述方式,來描繪網絡編程中常見的知識點,同時附帶代碼示例,后期會加上具體的抓包分析,實際項目、框架案例,希望可以和大家共同探索網絡世界。
什么是socket?
socket在網絡編程中的位置
-
- TCP是面向連接的、UDP是面向無連接的,所謂面向連接,指的就是通訊雙方會維護一組狀態,保證連接的可靠性。
- TCP的數據包結構相對復雜,而UDP則相對簡單。無論是TCP還是UDP,他們的包頭上都應該有端口號和目標端口號,TCP的包頭上有序號(順序),確認序號(不丟包)、窗口大小(流量控制 & 擁塞控制)、狀態碼(FIN ACK SYN)等。TCP和UDP的包頭如下圖。
- TCP數據傳輸方式是基於數據流的,而UDP則是基於數據報。
udp包頭

tcp包頭
如何使用socket:
代碼(TCP):
服務器:
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; /** * 服務器. * * @author jialin.li * @date 2019-12-10 19:45 */ public class TcpServer { public static void main(String[] args) throws IOException { int port = 8099; ServerSocket connectionSocket = new ServerSocket(port); // 監聽端口,accept為阻塞方法 System.out.println("Get socket successfully, wait for request..."); Socket communicationSocket = connectionSocket.accept(); // 獲取輸入流,讀取數據 InputStream inputStream = communicationSocket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String message; while ((message = bufferedReader.readLine()) != null) { System.out.printf("get message from client : %s",message); } communicationSocket.shutdownInput(); // 獲取輸出流,返回結果 OutputStream outputStream = communicationSocket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter); bufferedWriter.write("I got your message and the communication is over."); bufferedWriter.flush(); communicationSocket.shutdownOutput(); // 關閉資源 bufferedWriter.close(); bufferedReader.close(); } }
客戶端:
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.Socket; /** * 客戶端. * * @author jialin.li * @date 2019-12-10 22:30 */ public class TcpClient { public static void main(String[] args) throws IOException { // 指定ip 端口,創建socket String host = "127.0.0.1"; int port = 8099; Socket communicationSocket = new Socket(host, port); // 獲取輸出流,寫入數據 OutputStream outputStream = communicationSocket.getOutputStream(); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter); bufferedWriter.write("hello world"); bufferedWriter.flush(); communicationSocket.shutdownOutput(); // 獲取輸入流,讀取服務器返回信息 InputStream inputStream = communicationSocket.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String message; while ((message = bufferedReader.readLine()) != null) { System.out.printf("get message from server : %s", message); } communicationSocket.shutdownInput(); // 關閉資源 bufferedWriter.close(); bufferedReader.close(); } }
執行結果:
server:
Get socket successfully, wait for request...
get message from client : hello world
client:
get message from server : I got your message and the communication is over.
outputStream.close與shutdownOutput的區別:
這里只對outputStream進行分析,inputStream與之相同。
可以看出,上述代碼,我們在執行完輸入/輸出動作之后,會調用一個shutdownInput/shutdownOutput方法,這個方法有什么作用呢?可不可以用close方法替代?
首先,我們閱讀jdk中關於該方法的doc注釋:
Disables the output stream for this socket.
For a TCP socket, any previously written data will be sent
followed by TCP's normal connection termination sequence.
If you write to a socket output stream after invoking
shutdownOutput() on the socket, the stream will throw
an IOException.
大體意思是說,該方法會禁用socket的輸出流,對於基於TCP協議的Socket,任何在方法執行之前發送數據,都可以被正常發送,如果在這個方法執行后發送數據,就會拋出一個IO異常。通過該方法關閉流,Socket連接不會收到影響,但是如果我們直接使用close方法關閉流,那么Socket連接也會隨之關閉。接下來我們來測試一下,在Server中用inputStream.close來代替socket.shutdownInput方法。
結果是由於inputStream.close提前關閉了socket,導致服務器在輸出數據的時,socket.getOutputStream方法拋出異常:java.net.SocketException: Socket is closed
為什么我們每次調用BufferedWrite的write方法,都要調用flush方法:
這個問題不屬於網絡編程的范疇,但卻是我們在寫socket程序時,很容易犯的一個錯誤。bufferedWrite是字符緩存流,它的原理其實很簡單,在內存中設置一個緩存區,將原本逐個發送的字符緩存起來,批量發送(有很多框架也采用了這種思想,比如kafka的批量發送),flush方法是手動的將我們緩存區中的數據刷出。緩存區中的數據,將會在流關閉之前,進行flush。但是由於我們在發送數據之后,調用了shutdownOutput方法,導致最后close的時候,沒辦法將緩存區中的數據flush,因此會拋出一個寫入失敗的異常:java.net.SocketException: Broken pipe (Write failed)。
UDP是基於數據報的,因此不需要每對連接都建立一組socket,而是只要有一個socket,就能與多個客戶端通訊,所以只需要通過創建數據報,然后通過socket發送即可,具體步驟如下:

代碼(UDP):
服務器:
import java.net.DatagramPacket; import java.io.IOException; import java.net.DatagramSocket; /** * 服務器. * * @author jialin.li * @date 2019-12-10 19:45 */ public class UdpServer { public static void main(String[] args) throws IOException { // 監聽端口,阻塞方法 int port = 8099; DatagramSocket socket = new DatagramSocket(port); // 創建數據報,用於接收客戶端發送的數據 byte[] data = new byte[1024]; DatagramPacket packet = new DatagramPacket(data, data.length); // 接收客戶端發送的數據 System.out.println("Get socket successfully, wait for request..."); socket.receive(packet); String message = new String(data, 0, packet.getLength()); System.out.printf("get message from client : %s", message); // 向客戶端發送數據 byte[] data2 = "I got your message and the communication is over.".getBytes(); DatagramPacket packet2 = new DatagramPacket(data2, data2.length, packet.getAddress(), packet.getPort()); socket.send(packet2); socket.close(); } }
客戶端:
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * 客戶端. * * @author jialin.li * @date 2019-12-11 11:32 */ public class UdpClient { public static void main(String[] args) throws IOException { // 封裝數據報:ip、端口、數據 InetAddress ip = InetAddress.getByName("127.0.0.1"); int port = 8099; byte[] data = "hello world".getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length, ip, port); // 創建socket,發送數據報 DatagramSocket socket = new DatagramSocket(); socket.send(packet); // 讀取服務器返回信息 byte[] data2 = new byte[1024]; DatagramPacket packet2 = new DatagramPacket(data2, data2.length); socket.receive(packet2); // 讀取數據 String message = new String(data2, 0, packet2.getLength()); System.out.printf("get message from server : %s", message); //關閉資源 socket.close(); } }
執行結果:
server:
Get socket successfully, wait for request...
get message from client : hello world
client:
get message from server : I got your message and the communication is over.
