Java Socket編程基礎(1)


參考資料:

  《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();}
    }
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM