上篇文章:
網絡編程—Socket套接字(UDP)https://blog.csdn.net/sniper_fandc/article/details/146923670?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923670&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link
目錄
1 TCP流套接字
2 模擬實現TCP服務器
1 TCP流套接字
????????基于TCP的Socket主要有:ServerSocket和Socket,ServerSocket用于創建TCP服務器端的Socket,而Socket用于創建TCP客戶端的Socket。操作方式也類似文件。
構造方法/方法 | 含義 |
ServerSocket(int port) | 構造方法,創建一個服務端流套接字Socket,并綁定到指定端口 |
Socket?accept() | 普通方法,開始監聽指定端口(創建時綁定的端口),有客戶端連接后,返回一個服務端Socket對象,并基于該Socket建立與客戶端的連接,否則阻塞等待 |
void close() | 關閉TCP套接字 |
????????因為TCP是面向流的數據讀寫方式,因此沒有像DatagramPacket數據報的API,只需創建Socket后,采用類似InputStream和OutputStream的操作方式。也可以對InputStream和OutputStream進行Scanner和PrintWriter的包裝,便于字符數據的讀寫。
構造方法/方法 | 含義 |
Socket(String host, int?port) | 構造方法,創建一個客戶端流套接字Socket,并與對應IP的主機上,對應端口的進程建立連接 |
InetAddress getInetAddress() | 從套接字中獲取連接的IP地址 |
InputStream getInputStream() | 返回套接字中的輸入流(讀請求) |
OutputStream getOutputStream() | 返回套接字中的輸出流(寫響應) |
????????注意:Socket可能有兩種獲得的方式,1是使用Socket構造方法,2是使用ServerSocket的方法accept()。也就是說ServerSocket的主要作用就是創建TCP服務器的全局連接監聽,客戶端作為連接發起方,因此直接創建Socket表示申請建立連接,而ServerSocket的accept()方法一旦監聽到有客戶端申請建立連接,就返回一個Socket用于建立服務器和客戶端之間的連接。
????????上述分析方式也透露了ServerSocket和Socket的生命周期,ServerSocket的生命周期伴隨整個服務器進程,而Socket的生命周期只是一次連接周期。
2 模擬實現TCP服務器
public class TcpServer {//服務器端口號private final int PORT = 8000;//創建服務器private ServerSocket serverSocket = null;public TcpServer() throws IOException {serverSocket = new ServerSocket(PORT);}//啟動服務器public void start() throws IOException {System.out.println("服務器啟動成功");ExecutorService executorService = Executors.newCachedThreadPool();while(true){//將建立的TCP連接拿到應用程序中(accept()會阻塞,直到建立連接)Socket clientSocket = serverSocket.accept();//[版本1]直接調用processConnect()就會導致第一個客戶端連接執行到該方法while中,服務器線程從而無法執行accept//進而無法一個服務器為多個客戶端服務//[版本2]解決方案:多線程(一個線程accept(),一個線程processConnect())(新的問題:頻繁創建銷毀線程)// ???????????Thread t = new Thread(() ->{// ???????????????try {// ???????????????????processConnect(clientSocket);// ???????????????} catch (IOException e) {// ???????????????????e.printStackTrace();// ???????????????}// ???????????});// ???????????t.start();//[版本3]解決方案:線程池(新的問題:線程數量太多了(IO多路復用->NIO))executorService.submit(new Runnable() {@Overridepublic void run() {try {processConnect(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}//給當前連接的客戶端提供服務(一個連接只進行一次數據交互服務(短連接)||一個連接進行多次數據交互服務(長連接))//長連接版本(去掉循環就是短連接版本)public void processConnect(Socket clientSocket) throws IOException {System.out.printf("[%s:%d] 建立連接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){Scanner scanner = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while(true){if(!scanner.hasNext()){//如果沒有請求說明客戶端斷開連接System.out.printf("[%s:%d] 斷開連接\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());break;}//1.讀取請求并解析String request = scanner.next();//2.根據請求計算響應String response = process(request);//3.響應寫回客戶端// (注意此處不能使用next()類的函數,因為這類函數讀取結束標志是空白符:換行符、回車符等,輸入沒有這些符號服務器就會被阻塞在這類函數)printWriter.println(response);//刷新一下緩沖區printWriter.flush();System.out.printf("[%s:%d] request:%s, response:%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}}finally {//連接用完需要關閉(clientSocket生命周期是一次連接周期,而serverSocket生命周期是整個服務器運行周期)clientSocket.close();}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpServer tcpServer = new TcpServer();tcpServer.start();}}public class TcpClient {//創建客戶端private Socket socket = null;public TcpClient() throws IOException {//new對象時就是和TCP服務器建立連接(因此需要直到服務器地址)socket = new Socket("127.0.0.1",8000);}//啟動服務器public void start() throws IOException {Scanner scanner = new Scanner(System.in);try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()){Scanner scannerNet = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while(true){//1.讀取用戶輸入System.out.print(">");//注意此時next()讀取到換行就結束了,但是讀取的數據不含空白符,即沒有回車符String request = scanner.next();//2.發送請求// (注意此處不能使用next()類的函數,因為這類函數讀取結束標志是空白符:換行符、回車符等,輸入沒有這些符號服務器就會被阻塞在這類函數)printWriter.println(request);printWriter.flush();//3.接收響應String response = scannerNet.next();//4.將響應返回給用戶System.out.printf("request:%s, response:%s\n",request,response);}}}public static void main(String[] args) throws IOException {TcpClient tcpClient= new TcpClient();tcpClient.start();}}
運行結果如下:
????????上述代碼需要注意3點:
????????1.服務器端什么時候該關閉clientSocket(即關閉連接)?當服務器端processConnect方法內部從循環跳出時,證明此時客戶端沒有數據要發送,此時可以關閉連接,采用try-catch-finally方式,防止出現異常無法正常關閉。
????????2.如何處理next()引起的阻塞問題?上述代碼很多地方可能要用到Scanner的next()方法,但是該方法會讀取到空白符(回車換行等)才能結束,當客戶端輸入數據時可能不會攜帶空白符(在命令行中敲回車,該回車會被接收數據的next識別,發送的請求中并不攜帶回車符),此時就會導致服務器端一直未識別到結束,從而一直無響應。解決的辦法就是在發送的數據中添加空白符,比如使用println()方法會自動在數據結尾添加回車符。
????????3.如何解決服務器端只能為一個客戶端服務?當不采用多線程方案時,第一個客戶端建立連接發送請求,進入processConnect方法內部時,服務器端的主線程就會進入while中,從而其他客戶端申請建立連接時,服務器主線程無法通過accept()監聽建立連接的申請。采用多線程方案,線程池實現一個線程為一個客戶端服務(注意,當并發量很大時,線程池的線程數量很多,就會導致資源浪費調度困難等問題,此時需要采用NIO(非阻塞IO)的方式,這是一種I/O多路復用的技術,可以實現一個線程管理多個客戶端)。
下篇文章:
網絡編程—TCP/IP模型(UDP協議與自定義協議)https://blog.csdn.net/sniper_fandc/article/details/146923934?fromshare=blogdetail&sharetype=blogdetail&sharerId=146923934&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link