參考資料:
《Java網絡編程精解》 孫衛琴
一、socket通信簡介
什么是socket,簡單來說,在linux系統上,進程與進程之間的通信稱為IPC,在同一台計算機中,進程與進程之間通信可以通過信號、共享內存的方式等等。
不同計算機上的進程要進行通信的話就需要進行網絡通信,而 socket通信就是不同計算機進程間通信中常見的一種方式,當然,同一台計算機也可以通過socket進行通信,比如mysql支持通過unix socket本地連接。

socket在網絡系統中擁有以下作用:
(1) socket屏蔽了不同網絡協議之間的差異
(2) socket是網絡編程的入口,它提供了大量的系統調用system call供程序員使用
(3) linux的重要思想-一切皆文件,socket也是一種特殊的文件,網絡通信在linux系統上同樣是對文件的讀 寫操作
linux上支持多種套接字種類,不同的套接字種類稱為"地址簇",這是因為不同的套接字擁有不同的尋址方法。
linux將其抽象為統一的BSD套接字接口,從而屏蔽了它們的區別,程序員關心了只是BSD套接字接口而已。

以INET套接字為例:

Linux在利用socket()進行系統調用時,需要傳遞套接字的地址族標識符、套接字類型以及協議、源代碼:
asmlinkage long sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; retval = sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }
不過對於用戶而言,socket就是一種特殊的文件而已....
二、TCP/IP以及SOCKET通信簡介

linux上網絡通信實現由通信子網和資源子網2部分,
通信子網位於linux內核空間,由linux內核實現,例如netfilter, tcp/ip協議棧等等功能
資源子網由位於用戶空間的程序實現,例如httpd, nginx, haproxy等等。
計算機通信本質上是進程間的通信,一個計算機上可能運行着多個進程,我們使用端口來標記一個唯一的進程.
0~1023:管理員才有權限使用,永久地分配給某應用使用;
注冊端口:1024~41951:只有一部分被注冊,分配原則上非特別嚴格;
動態端口或私有端口:41952+:

tcp實現了以下功能:
①連接建立
②將數據打包成段 MTU通常為1500以下
校驗和
③確認、重傳以及超時機制
④排序
序列號 32位 並非從0開始 過大的話循環輪換 從0開始
⑤流量控制 速度不同步2台數據的服務器 防止阻塞
緩沖區 發送緩沖 接收緩沖
滑動窗口
⑥擁塞控制 多個進程通信
慢啟動 通過慢啟動的方式探測,啟動的時候很小 隨后以指數級增長。
擁塞避免算法

tcp是一個有限狀態機,三次連接,四次握手:

注意:如果server端沒有調用close()方法,可能出現大量連接處於CLOSE_WAIT狀態,占用系統資源。
三、Socket用法
在C/S通信模式中,客戶端主動創建與服務器連接的Socket,服務器收到了客戶端的連接請求,也會創建與客戶端連接的Socket。
Socket是通信連接兩端的收發器。服務器端監聽在某個固定的端口上,每當有一個客戶端連入時,都要創建一個socket文件,因此,linux系統打開文件數量直接影響着服務器端socket通信的並發能力。
3.1 構造器
當客戶端創建Socket連接Server時,會隨機分配端口,因此不用指定
public static void main(String[] args) throws Exception{ Socket socket = new Socket(); //遠程服務器地址 SocketAddress remoteAddr = new InetSocketAddress("localhost",8000); //設定超時時長,單位ms,為0表示永不超時,超時則跑出SocketTimeoutException socket.connect(remoteAddr,60*1000); }
設定客戶端地址:
在一個Socket對象中,同時包含了遠程服務器的ip地址,端口信息,也要包含客戶端的ip地址和端口信息,才能進行雙向通信。
默認,客戶端不設置ip的話,客戶端地址就是當前客戶端主機的地址。構造器中支持顯式指定。
Socket的創建和連接中出現的各種異常說明:
(1) UnkownHostException
無法識別主機名或者ip地址,找不到server主機
(2) ConnectException
2種情況:
沒有服務器進程監聽該端口
服務器進程拒絕連接:比如服務器端設置了請求隊列長度等情形。
(3) SocketTimeoutException
連接超時
(4) BindException
無法把Socket對象和指定的本地IP地址或者端口綁定,就會拋出這種異常
例如:socket.bind(new InetSocketAddress.getByName("222.34.5.7"),1234);
有可能本地主機沒有改地址,或者該端口不能被使用,就會拋出該異常。
3.2 獲取Socket信息
Socket包含了連接的相關信息,client和server的地址端口等等,還可以獲取InputStream和OutputStream,以下是一個demo
public class HTTPClient { String host="www.javathinker.org"; int port=80; Socket socket; public void createSocket()throws Exception{ socket=new Socket("www.javathinker.org",80); } public void communicate()throws Exception{ StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n"); sb.append("Host: www.javathinker.org\r\n"); sb.append("Accept: */*\r\n"); sb.append("Accept-Language: zh-cn\r\n"); sb.append("Accept-Encoding: gzip, deflate\r\n"); sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n"); sb.append("Connection: Keep-Alive\r\n\r\n"); //發出HTTP請求 OutputStream socketOut=socket.getOutputStream(); socketOut.write(sb.toString().getBytes()); socket.shutdownOutput(); //關閉輸出流 //接收響應結果 InputStream socketIn=socket.getInputStream(); ByteArrayOutputStream buffer=new ByteArrayOutputStream(); byte[] buff=new byte[1024]; int len=-1; while((len=socketIn.read(buff))!=-1){ buffer.write(buff,0,len); } System.out.println(new String(buffer.toByteArray())); //把字節數組轉換為字符串 /* InputStream socketIn=socket.getInputStream(); BufferedReader br=new BufferedReader(new InputStreamReader(socketIn)); String data; while((data=br.readLine())!=null){ System.out.println(data); } */ socket.close(); } public static void main(String args[])throws Exception{ HTTPClient client=new HTTPClient(); client.createSocket(); client.communicate(); } }
說明:上面方法用ByteArrayOutputStream來接收響應信息,也就是說響應會全部放置在內存中,在響應報文很長的時候這樣很不明智,上面注釋的代碼中演示了如何使用BufferReader逐行進行讀取。
3.3 關閉Socket
網絡通信占用資源且有太多的因素,在finally代碼塊中關閉socket是省事的
Socket類提供了3個狀態測試方法:
isClosed(): 如果Socket已經連接到遠程主機,並且還沒有關閉,則返回true
isConnected(): 如果Socket曾經連接到過遠程主機,返回true
isBound(): 如果Socket和本地端口綁定,返回true
因此確定一個Socket對象正在處於連接狀態,可以用以下方式
boolean isConnected = socket.isConnected() && !socket.isClosed();
3.4 半關閉Socket
socket通信也就是2個進程之間的通信,無論這2個進程是否處於同一個物理機器上,只需要向內核申請注冊了端口就可以用ip+port進行唯一的標識。
假設2個進程A和B之間通信,A如何通知B所有數據已經傳輸完畢呢?
以上文中HttpClient為例
StringBuffer sb=new StringBuffer("GET "+"/index.jsp"+" HTTP/1.1\r\n"); sb.append("Host: www.javathinker.org\r\n"); sb.append("Accept: */*\r\n"); sb.append("Accept-Language: zh-cn\r\n"); sb.append("Accept-Encoding: gzip, deflate\r\n"); sb.append("User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\r\n"); sb.append("Connection: Keep-Alive\r\n\r\n");
這實際上是典型的HTTP處理的方式,沒有請求實體,因此以\r\n\r\n表示結束,這就是一種約定方式。
(1) 如果是字符流,可以以特殊字符作為結束標志,可以是\r\n\r\n,甚至於可以定義為"bye"
(2) A可以先發送一個消息,事先聲明了內容長度
(3) A發送完畢之后,主動關閉Socket,B讀取完了所有數據也關閉
(4) shutdownInput, shutdownOutput 之關閉輸出流或者輸出流,但是這並不會釋放資源,必須調用Socket的close()方法,才會釋放資源
3.5 Socket常用選項
TCP_NODELAY: 表示立即發送數據,默認是false,表示開啟Negale算法,true表示關閉緩沖,確保數據及時發送
為false時,適合發送方需要發送大批量數據,並且接收方及時響應,這種算法通過減少傳輸數據的次數來提高效率
為true,發送方持續的發送小批量數據,並且接受方不一定會立即響應數據
SO_REUSEADDR: 表示是否允許重用Socket綁定的本地地址
SO_TIMEOUT: 表示接收數據的等待超時時間
SO_LINGER: 表示執行Socket的close()方法時,是否立即關閉底層的Socket,哪怕還有數據沒有發送完也直接關閉
SO_SNFBUF: 發送方緩沖區大小
SO_RCVBUF: 接收數據的緩沖區大小
SO_KEEPALIVE: 對於長時間處於空閑狀態的Socket是否要自動關閉
四、ServerSocket用法
在C/S架構中,服務器端需要創建監聽特定端口的ServerSocket,ServerSocket負責接收客戶的連接請求。
4.1 ServerSocket
1.必須綁定一個端口
ServerSocket serverSocket = new ServerSocket(80);
如果無法綁定到一個端口,會拋出BindException,一般由以下原因:
(1) 端口已經被占用
(2) 某些操作系統中,只有超級用戶才允許使用1-1023的端口
如果port設置為0,表示操作系統來分配一個任意可用的端口,匿名端口,在某些場合,匿名端口有特殊作用
2. 設定客戶連接請求隊列的長度
一般的C/S架構中,服務器監聽在某個固定的端口上,每來一個客戶端連接,服務器都會創建一個socket文件維護與client的通信
管理client連接的任務往往由操作系統來完成。操作系統把這些連接請求存儲在一個先進先出的隊列中。
許多操作系統限定了隊列的最大長度,一般是50。當client connections>50 時,服務器會拒絕新的請求。
對於客戶端而言,如果他的請求被server加入了隊列,意味着連接成功,這個隊列通常稱為backlog.
ServerSocket構造方法的backlog參數用來顯示指定連接請求隊列的長度,它將覆蓋操作系統限定的最大長度,不過在以下情形,依舊采用操作系統的默認值:
(1) backlog <= 0
(2) without setting backlog
(3) backlog參數的值 > 操作系統的允許范圍
演示: Server端設置backlog為3,不處理請求,client連接超過3會拒絕
import java.io.*; import java.net.*; public class Server { private int port=8000; private ServerSocket serverSocket; public Server() throws IOException { serverSocket = new ServerSocket(port,3); //連接請求隊列的長度為3 System.out.println("服務器啟動"); } public void service() { while (true) { Socket socket=null; try { socket = serverSocket.accept(); //從連接請求隊列中取出一個連接 System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } } } public static void main(String args[])throws Exception { Server server=new Server(); Thread.sleep(60000*10); //睡眠十分鍾 //server.service(); } }
import java.net.*; public class Client { public static void main(String args[])throws Exception{ final int length=100; String host="localhost"; int port=8000; Socket[] sockets=new Socket[length]; for(int i=0;i<length;i++){ //試圖建立100次連接 sockets[i]=new Socket(host, port); System.out.println("第"+(i+1)+"次連接成功"); } Thread.sleep(3000); for(int i=0;i<length;i++){ sockets[i].close(); //斷開連接 } } }
3. 設定綁定的IP地址
一個主機可能有多個地址,此時可以顯示指定
ServerSocket serverSocket = new ServerSocket(); // 只有在設定地址之前設置才有效 serverSocket.setReuseAddress(true); serverSocket.bind(new InetSocketAddress(8000));
4. 關閉ServerSocket
同樣應該在finally代碼塊中調用close()方法,在一般的連接中,往往是由客戶端發起請求,也是由客戶端發起關閉socket請求。
但是,在某些keepalive的場景中,例如httpd,nginx等等服務器都支持長連接,通過設定keepalive的最大連接時長和最大連接數來控制長連接。
此時,那些由於超時的client連接,服務器端會主動發起close()請求。
如何判斷ServerSocket沒有關閉
boolean isOpen = serverSocket.isBound() && !serverSocket.isClosed();
4.2 ServerSocket選項
1. SO_TIMEOUT
accept()方法等待客戶端的連接超時時間,以ms為單位,0表示永不超時,默認是0.
當執行accept()時,如果backlog為空,則服務器一直等待,如果設置了超時時間,則服務器端阻塞在此,超時則拋出SocketTimeoutException
2. SO_REUSEADDR選項
當服務器因為某些原因需要重啟時,如果網絡上還有發送到這個ServerSocket的數據,則ServerSocket不會立刻釋放該端口,導致重啟失敗。
設置為true的話可以確保釋放,但是必須在綁定端口之前調用方法。
3. SO_RCVBUF
接收緩沖大小
五、Demo
import java.io.*; import java.net.*; import java.util.concurrent.*; public class EchoServer { private int port=8000; private ServerSocket serverSocket; private ExecutorService executorService; //線程池 private final int POOL_SIZE=4; //單個CPU時線程池中工作線程的數目 private int portForShutdown=8001; //用於監聽關閉服務器命令的端口 private ServerSocket serverSocketForShutdown; private boolean isShutdown=false; //服務器是否已經關閉 private Thread shutdownThread=new Thread(){ //負責關閉服務器的線程 public void start(){ this.setDaemon(true); //設置為守護線程(也稱為后台線程) super.start(); } public void run(){ while (!isShutdown) { Socket socketForShutdown=null; try { socketForShutdown= serverSocketForShutdown.accept(); BufferedReader br = new BufferedReader( new InputStreamReader(socketForShutdown.getInputStream())); String command=br.readLine(); if(command.equals("shutdown")){ long beginTime=System.currentTimeMillis(); socketForShutdown.getOutputStream().write("服務器正在關閉\r\n".getBytes()); isShutdown=true; //請求關閉線程池 //線程池不再接收新的任務,但是會繼續執行完工作隊列中現有的任務 executorService.shutdown(); //等待關閉線程池,每次等待的超時時間為30秒 while(!executorService.isTerminated()) executorService.awaitTermination(30,TimeUnit.SECONDS); serverSocket.close(); //關閉與EchoClient客戶通信的ServerSocket long endTime=System.currentTimeMillis(); socketForShutdown.getOutputStream().write(("服務器已經關閉,"+ "關閉服務器用了"+(endTime-beginTime)+"毫秒\r\n").getBytes()); socketForShutdown.close(); serverSocketForShutdown.close(); }else{ socketForShutdown.getOutputStream().write("錯誤的命令\r\n".getBytes()); socketForShutdown.close(); } }catch (Exception e) { e.printStackTrace(); } } } }; public EchoServer() throws IOException { serverSocket = new ServerSocket(port); serverSocket.setSoTimeout(60000); //設定等待客戶連接的超過時間為60秒 serverSocketForShutdown = new ServerSocket(portForShutdown); //創建線程池 executorService= Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors() * POOL_SIZE); shutdownThread.start(); //啟動負責關閉服務器的線程 System.out.println("服務器啟動"); } public void service() { while (!isShutdown) { Socket socket=null; try { socket = serverSocket.accept(); //可能會拋出SocketTimeoutException和SocketException socket.setSoTimeout(60000); //把等待客戶發送數據的超時時間設為60秒 executorService.execute(new Handler(socket)); //可能會拋出RejectedExecutionException }catch(SocketTimeoutException e){ //不必處理等待客戶連接時出現的超時異常 }catch(RejectedExecutionException e){ try{ if(socket!=null)socket.close(); }catch(IOException x){} return; }catch(SocketException e) { //如果是由於在執行serverSocket.accept()方法時, //ServerSocket被ShutdownThread線程關閉而導致的異常,就退出service()方法 if(e.getMessage().indexOf("socket closed")!=-1)return; }catch(IOException e) { e.printStackTrace(); } } } public static void main(String args[])throws IOException { new EchoServer().service(); } } class Handler implements Runnable{ private Socket socket; public Handler(Socket socket){ this.socket=socket; } private PrintWriter getWriter(Socket socket)throws IOException{ OutputStream socketOut = socket.getOutputStream(); return new PrintWriter(socketOut,true); } private BufferedReader getReader(Socket socket)throws IOException{ InputStream socketIn = socket.getInputStream(); return new BufferedReader(new InputStreamReader(socketIn)); } public String echo(String msg) { return "echo:" + msg; } public void run(){ try { System.out.println("New connection accepted " + socket.getInetAddress() + ":" +socket.getPort()); BufferedReader br =getReader(socket); PrintWriter pw = getWriter(socket); String msg = null; while ((msg = br.readLine()) != null) { System.out.println(msg); pw.println(echo(msg)); if (msg.equals("bye")) break; } }catch (IOException e) { e.printStackTrace(); }finally { try{ if(socket!=null)socket.close(); }catch (IOException e) {e.printStackTrace();} } } }
