java多線程網絡編程——探究java socket與linux socket

  在當今互聯網時代,網絡顯得尤其重要,不管是QQ、微信,仍是網絡遊戲,都離不開網絡通訊,而java做爲當web開發最火的語言,相信你們都接觸過java網絡編程,那java網絡通訊中調用了系統級的哪些接口呢?今天,我就帶着你們共同探究java socket與linux socket之間的千絲萬縷。java

  說到網絡通訊怎麼能不談計算機網絡呢,簡而言之,網絡界主要有兩種網絡分層模型:即OSI和TCP/IP,OSI有7層,TCP/IP則將網絡分爲4層,如今TCP/IP模型是事實上的網絡標準,而咱們結合二者,通常都說TCP/IP 5層協議模型,下面給一張圖來講明:linux

  

 

  那socket接口在哪一層呢,事實上socket是系統爲咱們提供的網絡通訊接口,若是非要說它在哪一層的話,那麼socket就位於應用層和傳輸層之間,經過socket接口對網絡的抽象,屏蔽了下面各層那麼多複雜的協議,給人感受好像是用socket套接字直接與對方通訊同樣,這樣大大簡化了程序員的工做,使得程序員根本不須要關心底層的東西,只須要經過socket接口與應用層和傳輸層打交道便可,固然其實大部分時間程序員只須要關心應用層便可。通常來說,傳輸層有兩大協議,即面向鏈接的TCP和無鏈接的UDP協議,所謂面向鏈接是指傳輸是有序的、無差錯的,可能更費時,但頗有用;而無鏈接是指盡最大努力交付,出點差錯也無所謂。程序員

  socket會用到運輸層的服務,那麼固然socket接口也有基於tcp的socket和基於udp的socket之分,udp比較簡單,今天就以基於tcp的socket爲例,使用java語言編寫一個socket多線程網絡聊天程序,探究java socket背後的工做原理。web

 

  在編寫代碼以前先簡單介紹下java網絡編程中最重要的socket接口:  編程

  一、Scoket又稱「套接字」,其由IP地址和端口號組成,能夠說它惟一標識了網絡上的某個進程,應用程序一般經過「套接字」向網絡發出請求或者應答網絡請求;在 java中Socket和ServerSocket類庫位於java.net包中。ServerSocket用於服務器端,Socket是創建網絡鏈接時使用的,在鏈接成功時,應用程序兩端都會產生一個Socket實例,操做這個實例,完成所需的會話。對於一個網絡鏈接來講,套接字是平等的,並無差異,不由於在服務器端或在客戶端而產生不一樣的級別,不論是Socket仍是ServerSocket他們的工做都是經過Socket類和其子類來完成的服務器

  二、創建Socket連接可分三個步驟:
         1.服務器監聽
         2.客戶端發出請求
         3.創建連接
         4.通訊
  三、Socket特色:
          1.基於TCP連接,數據傳輸有保障
          2.適用於創建長時間的連接,不像HTTP那樣會隨機關閉
          3.Socket編程應用於即時通信
微信

  通俗點講,socket是一個電話號碼,每一個手機都有一個電話號碼,當你想打電話給對方時,首先要知道對方的電話號碼即socket,對方的手機要保證有話費的,當播出號碼的時候,對方手機響了,按下吉接聽鍵,這是就創建了雙方的鏈接,就能雙方相互通話了。網絡

  下面進入正題,開始寫java多線程網絡聊天程序,首先是服務器代碼:多線程

package socket; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; /*** * 多線程TCP服務器,爲每一個鏈接創立一個線程 * @author mjc * @version 1.1 2019-12-4 */ public class TCPServer { public static void main(String[] args){ try(ServerSocket s = new ServerSocket(8189)) { int i = 1; while (true){ Socket incoming = s.accept(); System.out.println("鏈接序號:"+i); Runnable r = new ServerThread(incoming); Thread t = new Thread(r); t.start(); i++; } } catch (IOException e) { e.printStackTrace(); } } } class ServerThread implements Runnable{ private Socket incoming; public ServerThread(Socket incoming){ this.incoming = incoming; } public void run(){ try(InputStream inputStream = incoming.getInputStream(); OutputStream outputStream = incoming.getOutputStream()){ Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); //out.println("Hello! Enter BYE to exit."); boolean done = false; while (!done&&in.hasNextLine()){ String line = in.nextLine(); System.out.println("客戶端發來: "+line); //out.println("Echo: "+line); if(line.trim().equals("BYE")) { System.out.println("我發給客戶端: BYE,BYE!"); System.out.println("與客戶端鏈接斷開"); out.println("BYE,BYE!"); done = true;} else System.out.println("我發給客戶端: hi!"); out.println("hi!"); } } catch (IOException e) { e.printStackTrace(); } } }
  服務器主要設計了兩個類,一個是TCPServer,一個是實現了Runnable接口的線程類ServerThread,用來實現多線程,在主方法中,首先用ServerSocket s = new ServerSocket(8189)
建立一個端口爲8189的監聽端口,而後循環使用Socket incoming = s.accept();接受客戶端的鏈接並創建相應的socket,每創建一個鏈接便啓動一個服務器線程,這樣即可以和多個客戶端進行通訊。
服務器主要是收到客戶端的字符串,並回送一個hi,直到客戶端發出BYE,服務器便向對方回送BYE,BYE!,而後斷開與客戶端的鏈接。
  接下來編寫兩個客戶端程序,將同時與服務器進行通訊,客戶端1源碼:
package socket; import java.io.*; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; public class TCPClient1 { public static void main(String[] args){ try (Socket s = new Socket("127.0.0.1",8189); InputStream inputStream = s.getInputStream(); OutputStream outputStream = s.getOutputStream()) { System.out.println("客戶端1鏈接到服務器成功!"); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); System.out.println("開始與服務器聊天,說BYE去結束聊天."); boolean done =false; while(!done){ String line = br.readLine(); if(line.equals("BYE")) done = true; out.println(line); System.out.println("服務器說: "+in.nextLine()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

  客戶端2源碼:dom

package socket; import java.io.*; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; public class TCPClient2 { public static void main(String[] args){ try (Socket s = new Socket("127.0.0.1",8189); InputStream inputStream = s.getInputStream(); OutputStream outputStream = s.getOutputStream()) { System.out.println("客戶端2鏈接到服務器成功!"); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); Scanner in = new Scanner(inputStream,"GBK"); PrintWriter out = new PrintWriter(new OutputStreamWriter(outputStream,"GBK"),true); System.out.println("開始與服務器聊天,說BYE去結束聊天."); boolean done =false; while(!done){ String line = br.readLine();
          System.out.println("客戶端發來: "+line);
if(line.equals("BYE"))
            System.out.println("我發給客戶端: BYE,BYE!"); done
= true; out.println(line); System.out.println("服務器說: "+in.nextLine()); } } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

  客戶端首先使用 Socket s = new Socket("127.0.0.1",8189);創建了一個對方ip爲127.0.0.1(即本地主機),端口爲8189的socket,客戶端會向這個socket發出創建請求,若是創建成功則返回一個socket s,用戶可在命令行敲出字符串,這個消息會發送到指定地址的服務器進程,當客戶端輸入BYE的時候,服務器會回送一個BYE,BYE!而後斷開與鏈接。

  如今讓它們跑起來試試,先開啓服務端,而後開啓兩個客戶端:

  如今在客戶端輸入BYE試試:

 

 

  能夠看到,用java來寫網絡通訊程序仍是比較簡單的,服務端只用到了 ServerSocket類及其accept()方法和socket類,客戶端也就用到了socket類,這樣二者便能通暢的對話了,java語言爲咱們提供的網絡編程API讓咱們沒必要關心底層的細節,然而其實它的通訊也是利用了系統的socket API,在探究java的socket以前咱們先來看看linux 爲咱們提供的socket API:

  這裏再提一次,socket就是抽象封裝了傳輸層如下軟硬件行爲,爲上層應用程序提供進程/線程間通訊管道。就是讓應用開發人員不用管信息傳輸的過程,直接用socket API就OK了。貼個TCP的socket示意圖體會一下:

  

 

   如今以TCP client/server模型爲例子看一下linux socket通訊的整個過程:

socket API函數以下:

socket: establish socket interface
gethostname: obtain hostname of system
gethostbyname: returns a structure of type hostent for the given host name
bind: bind a name to a socket
listen: listen for connections on a socket
accept: accept a connection on a socket
connect: initiate a connection on a socket
setsockopt: set a particular socket option for the specified socket.
close: close a file descriptor
shutdown: shut down part of a full-duplex connection

1. socket()

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); - 參數說明 domain: 設定socket雙方通訊協議域,是本地/internet ip4 or ip6 Name Purpose Man page AF_UNIX, AF_LOCAL Local communication unix(7) AF_INET IPv4 Internet protocols ip(7) AF_INET6 IPv6 Internet protocols ipv6(7) type: 設定socket的類型,經常使用的有 SOCK_STREAM - 通常對應TCP、sctp SOCK_DGRAM - 通常對應UDP SOCK_RAW - protocol: 設定通訊使用的傳輸層協議 經常使用的協議有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,能夠設置爲0,系統本身選定。注意protocol和type不是隨意組合的。

 

  socket() API是在glibc中實現的,該函數又調用到了kernel的sys_socket(),調用鏈以下:

 

 

 

  詳細的kernel實現我沒有去讀,大致上這樣理解。調用socket()會在內核空間中分配內存而後保存相關的配置。同時會把這塊kernel的內存與文件系統關聯,之後即可以經過filehandle來訪問修改這塊配置或者read/write socket。操做socket就像操做file同樣,應了那句unix一切皆file。提示系統的最大filehandle數是有限制的,/proc/sys/fs/file-max設置了最大可用filehandle數。

2. bind()

#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 參數說明 sockfd:以前socket()得到的file handle addr:綁定地址,可能爲本機IP地址或本地文件路徑 addrlen:地址長度 功能說明 bind()設置socket通訊的地址,若是爲INADDR_ANY則表示server會監聽本機上全部的interface,若是爲127.0.0.1則表示監聽本地的process通訊(外面的process也接不進啊)。

3. listen()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog); 參數說明 sockfd:以前socket()得到的file handle backlog:設置server能夠同時接收的最大連接數,server端會有個處理connection的queue,listen設置這個queue的長度。 功能說明 listen()只用於server端,設置接收queue的長度。若是queue滿了,server端能夠丟棄新到的connection或者回復客戶端ECONNREFUSED。

4. accept()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 參數說明: addr:對端地址 addrlen:地址長度 功能說明: accept()從queue中拿出第一個pending的connection,新建一個socket並返回。 新建的socket咱們叫connected socket,區別於前面的listening socket。 connected socket用來server跟client的後續數據交互,listening socket繼續waiting for new connection。 當queue裏沒有connection時,若是socket經過fcntl()設置爲 O_NONBLOCK,accept()不會block,不然通常會block。

5. connect()

 #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 參數說明: sockfd: socket的標示filehandle addr:server端地址 addrlen:地址長度 功能說明: connect()用於雙方鏈接的創建。 對於TCP鏈接,connect()實際發起了TCP三次握手,connect成功返回後TCP鏈接就創建了。 對於UDP,因爲UDP是無鏈接的,connect()能夠用來指定要通訊的對端地址,後續發數據send()就不須要填地址了。 固然UDP也能夠不使用connect(),socket()創建後,在sendto()中指定對端地址。

   以上就是系統爲咱們提供的主要socket接口函數以及C/S模型使用TCP通訊的過程,這些函數都是用C語言實現的,java底層也是用C語言寫的,

  如今讓咱們來追蹤java網絡程序中調用的socket接口過程:

  上面的客戶端只是實例化Socket類即可向對方創建鏈接,就先從Socket談起吧,在Idea IDE中追蹤Socket:

(1)起始、

Socket s = new Socket("127.0.0.1",8189)

(2)追蹤Socket、

 public Socket(String host, int port) throws UnknownHostException, IOException { this(host != null ? new InetSocketAddress(host, port) : new InetSocketAddress(InetAddress.getByName((String)null), port), (SocketAddress)null, true); }

(3)發如今調用構造方法中,又調用了構造函數,跟蹤這個this()構造函數:

private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException { this.created = false; this.bound = false; this.connected = false; this.closed = false; this.closeLock = new Object(); this.shutIn = false; this.shutOut = false; this.oldImpl = false; this.setImpl(); if (address == null) { throw new NullPointerException(); } else { try { this.createImpl(stream); if (localAddr != null) { this.bind(localAddr); } this.connect(address); } catch (IllegalArgumentException | SecurityException | IOException var7) { try { this.close(); } catch (IOException var6) { var7.addSuppressed(var6); } throw var7; } } }

  ok,終於找到你了,這個構造函數中產生了一個流,先無論這個,能夠認爲是一個通訊的管道,重點是這裏調用了 this.bind(localAddr)和 this.connect(address)方法,是否是很熟悉,沒錯跟linux socket接口函數同樣,一個用來綁定地址並監聽,一個用來向服務端請求鏈接。

  如今再來跟蹤下服務端的ServerSocket類和accept()方法:

(1)起始、

ServerSocket s = new ServerSocket(8189)

(2)跟蹤ServerSocket、

public ServerSocket(int port) throws IOException { this(port, 50, (InetAddress)null); }

(3)跟蹤這個this構造方法:

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException { this.created = false; this.bound = false; this.closed = false; this.closeLock = new Object(); this.oldImpl = false; this.setImpl(); if (port >= 0 && port <= 65535) { if (backlog < 1) { backlog = 50; } try { this.bind(new InetSocketAddress(bindAddr, port), backlog); } catch (SecurityException var5) { this.close(); throw var5; } catch (IOException var6) { this.close(); throw var6; } } else { throw new IllegalArgumentException("Port value out of range: " + port); } }

  能夠看到,先判斷端口號是否合理,而後調用了 this.bind()方法綁定地址並開始監聽這個端口;

(4)跟蹤bind()方法:

 

public void bind(SocketAddress endpoint, int backlog) throws IOException { if (this.isClosed()) { throw new SocketException("Socket is closed"); } else if (!this.oldImpl && this.isBound()) { throw new SocketException("Already bound"); } else { if (endpoint == null) { endpoint = new InetSocketAddress(0); } if (!(endpoint instanceof InetSocketAddress)) { throw new IllegalArgumentException("Unsupported address type"); } else { InetSocketAddress epoint = (InetSocketAddress)endpoint; if (epoint.isUnresolved()) { throw new SocketException("Unresolved address"); } else { if (backlog < 1) { backlog = 50; } try { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkListen(epoint.getPort()); } this.getImpl().bind(epoint.getAddress(), epoint.getPort()); this.getImpl().listen(backlog); this.bound = true; } catch (SecurityException var5) { this.bound = false; throw var5; } catch (IOException var6) { this.bound = false; throw var6; } } } } }

 

  發現了什麼,bind()函數裏又調用了listen()方法;簡直和linux socket通訊過程如出一轍啊。

this.getImpl().listen(backlog);

 

  接下來看看 Socket incoming = s.accept()又作了什麼:

(1)起始:

Socket incoming = s.accept();

(2)跟蹤accept():

public Socket accept() throws IOException { if (this.isClosed()) { throw new SocketException("Socket is closed"); } else if (!this.isBound()) { throw new SocketException("Socket is not bound yet"); } else { Socket s = new Socket((SocketImpl)null); this.implAccept(s); return s; } }

  能夠看到accept接受鏈接並返回一個socket對象,服務端即可利用這個socket對象與客戶端通訊。

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

  總結:本次帶着你們使用java提供的socket編寫了多線程的網絡聊天程序,經過對java socket接口調用的一步步跟蹤,咱們發現雖然使用java socket編程很是簡單,可是其內部也是調用了一系列的如同linux socket通訊的socket函數,廢話很少說,用圖來直觀的感覺一下:

 

 

   上圖是服務端的java socket調用過程,即當咱們在java建立一個tcp鏈接時,須要首先實例化java的ServerSocket類,其中封裝了底層的socket()方法、bind()方法、listen()方法。

  客戶端java經過使用實例化Socket對象向服務端請求創建鏈接,在實例化Socket對象時,一樣調用了與linux socket API同樣的socket()、connect()方法,便可以說是java客戶端中的Socket封裝了linux socket中的socket()、connect()方法,經過java socket的這種封裝屏蔽了底層一些咱們看不到的socket 調用過程,這就對程序員顯得更加友好了,可是做爲一個計算機專業的學生,咱們不能只使用「黑盒子」,而不去打開「黑盒子」去看看其內部構造,只有挖掘到事物內部、知其然,知其因此然,咱們才能創造屬於咱們本身的「黑盒子」!(碼了一天了,求支持一波)

相關文章
相關標籤/搜索