基於Socket的java網絡編程

1,什麼是Socket

網絡上的兩個程序經過一個雙向的通信鏈接實現數據的交換,這個雙向鏈路的一端稱爲一個Socket。Socket一般用來實現客戶方和服務方的鏈接。Socket是TCP/IP協議的一個十分流行的編程界面,一個Socket由一個IP地址和一個端口號惟一肯定java

2,Socket通信的過程

對於一個功能齊全的Socket,都要包含如下基本結構,其工做過程包含如下四個基本的步驟:編程

  (1) 建立Socket;服務器

  (2) 打開鏈接到Socket的輸入/出流;網絡

  (3) 按照必定的協議對Socket進行讀/寫操做;app

  (4) 關閉Socket.(在實際應用中,並未使用到顯示的close,雖然不少文章都推薦如此,不過在個人程序中,可能由於程序自己比較簡單,要求不高,因此並未形成什麼影響。)dom

3,建立Socket

建立Socketsocket

java在包java.net中提供了兩個類Socket和ServerSocket,分別用來表示雙向鏈接的客戶端和服務端。這是兩個封裝得很是好的類,使用很方便。其構造方法以下:tcp

  Socket(InetAddress address, int port);函數

  Socket(InetAddress address, int port, boolean stream);學習

  Socket(String host, int prot);

  Socket(String host, int prot, boolean stream);

  Socket(SocketImpl impl)

  Socket(String host, int port, InetAddress localAddr, int localPort)

  Socket(InetAddress address, int port, InetAddress localAddr, int localPort)

  ServerSocket(int port);

  ServerSocket(int port, int backlog);

  ServerSocket(int port, int backlog, InetAddress bindAddr)

  其中address、host和port分別是雙向鏈接中另外一方的IP地址、主機名和端 口號,stream指明socket是流socket仍是數據報socket,localPort表示本地主機的端口號,localAddr和 bindAddr是本地機器的地址(ServerSocket的主機地址),impl是socket的父類,既能夠用來建立serverSocket又可 以用來建立Socket。count則表示服務端所能支持的最大鏈接數。例如:學習視頻網 http://www.xxspw.com

  Socket client = new Socket("127.0.01.", 80);

  ServerSocket server = new ServerSocket(80);

  注意,在選擇端口時,必須當心。每個端口提供一種特定的服務,只有給出正確的端口,才 能得到相應的服務。0~1023的端口號爲系統所保留,例如http服務的端口號爲80,telnet服務的端口號爲21,ftp服務的端口號爲23, 因此咱們在選擇端口號時,最好選擇一個大於1023的數以防止發生衝突。

  在建立socket時若是發生錯誤,將產生IOException,在程序中必須對之做出處理。因此在建立Socket或ServerSocket是必須捕獲或拋出例外。

三、socket的基本操做

既然socket是「open—write/read—close」模式的一種實現,那麼socket就提供了這些操做對應的函數接口。下面以TCP爲例,介紹幾個基本的socket接口函數。

3.一、socket()函數

int socket(int domain, int type, int protocol);

socket函數對應於普通文件的打開操做。普通文件的打開操做返回一個文件描述字,而socket()用於建立一個socket描述符(socket descriptor),它惟一標識一個socket。這個socket描述字跟文件描述字同樣,後續的操做都有用到它,把它做爲參數,經過它來進行一些讀寫操做。

正如能夠給fopen的傳入不一樣參數值,以打開不一樣的文件。建立socket的時候,也能夠指定不一樣的參數建立不一樣的socket描述符,socket函數的三個參數分別爲:

  • domain:即協議域,又稱爲協議族(family)。經常使用的協議族有,AF_INETAF_INET6AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通訊中必須採用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名做爲地址。

  • type:指定socket類型。經常使用的socket類型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等(socket的類型有哪些?)。

  • protocol:故名思意,就是指定協議。經常使用的協議有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(這個協議我將會單獨開篇討論!)。

注意:並非上面的type和protocol能夠隨意組合的,如SOCK_STREAM不能夠跟IPPROTO_UDP組合。當protocol爲0時,會自動選擇type類型對應的默認協議。

當咱們調用socket建立一個socket時,返回的socket描述字它存在於協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。若是想要給它賦值一個地址,就必須調用bind()函數,不然就當調用connect()listen()時系統會自動隨機分配一個端口。

3.二、bind()函數

正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INETAF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函數的三個參數分別爲:

  • sockfd:即socket描述字,它是經過socket()函數建立了,惟一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。

  • addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不一樣而不一樣,如ipv4對應的是: 

    struct sockaddr_in {
       sa_family_t    sin_family; /* address family: AF_INET */
       in_port_t      sin_port;   /* port in network byte order */
       struct in_addr sin_addr;   /* internet address */};/* Internet address. */struct in_addr {
       uint32_t       s_addr;     /* address in network byte order */};

    ipv6對應的是: 

    struct sockaddr_in6 { 
       sa_family_t     sin6_family;   /* AF_INET6 */
       in_port_t       sin6_port;     /* port number */
       uint32_t        sin6_flowinfo; /* IPv6 flow information */
       struct in6_addr sin6_addr;     /* IPv6 address */
       uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ };struct in6_addr {
       unsigned char   s6_addr[16];   /* IPv6 address */ };

    Unix域對應的是: 

    #define UNIX_PATH_MAX    108struct sockaddr_un { 
       sa_family_t sun_family;               /* AF_UNIX */
       char        sun_path[UNIX_PATH_MAX];  /* pathname */ };
  • addrlen:對應的是地址的長度。

一般服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就能夠經過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲何一般服務器端在listen以前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。

網絡字節序與主機字節序

主機字節序就是咱們日常說的大端和小端模式:不一樣的CPU有不一樣的字節序類型,這些字節序是指整數在內存中保存的順序,這個叫作主機序。引用標準的Big-Endian和Little-Endian的定義以下:

  a) Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。

  b) Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。

網絡字節序:4個字節的32 bit值如下面的次序傳輸:首先是0~7bit,其次8~15bit,而後16~23bit,最後是24~31bit。這種傳輸次序稱做大端字節序。因爲TCP/IP首部中全部的二進制整數在網絡中傳輸時都要求以這種次序,所以它又稱做網絡字節序。字節序,顧名思義字節的順序,就是大於一個字節類型的數據在內存中的存放順序,一個字節的數據沒有順序的問題了。

因此:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,而不要假定主機字節序跟網絡字節序同樣使用的是Big-Endian。因爲這個問題曾引起過血案!公司項目代碼中因爲存在這個問題,致使了不少莫名其妙的問題,因此請謹記對主機字節序不要作任何假定,務必將其轉化爲網絡字節序再賦給socket。

3.三、listen()、connect()函數

若是做爲一個服務器,在調用socket()bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。

int listen(int sockfd, int backlog);int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen函數的第一個參數即爲要監聽的socket描述字,第二個參數爲相應socket能夠排隊的最大鏈接個數。socket()函數建立的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的鏈接請求。

connect函數的第一個參數即爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。客戶端經過調用connect函數來創建與TCP服務器的鏈接。

3.四、accept()函數

TCP服務器端依次調用socket()bind()listen()以後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()connect()以後就想TCP服務器發送了一個鏈接請求。TCP服務器監聽到這個請求以後,就會調用accept()函數取接收請求,這樣鏈接就創建好了。以後就能夠開始網絡I/O操做了,即類同於普通文件的讀寫I/O操做。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函數的第一個參數爲服務器的socket描述字,第二個參數爲指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數爲協議地址的長度。若是accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,表明與返回客戶的TCP鏈接。

注意:accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字;而accept函數返回的是已鏈接的socket描述字。一個服務器一般一般僅僅只建立一個監聽socket描述字,它在該服務器的生命週期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立了一個已鏈接socket描述字,當服務器完成了對某個客戶的服務,相應的已鏈接socket描述字就被關閉。

3.五、read()、write()等函數

萬事具有隻欠東風,至此服務器與客戶已經創建好鏈接了。能夠調用網絡I/O進行讀寫操做了,即實現了網咯中不一樣進程之間的通訊!網絡I/O操做有下面幾組:

  • read()/write()

  • recv()/send()

  • readv()/writev()

  • recvmsg()/sendmsg()

  • recvfrom()/sendto()

我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上能夠把上面的其它函數都替換成這兩個函數。它們的聲明以下:

       #include <unistd.h>

      ssize_t read(int fd, void *buf, size_t count);
      ssize_t write(int fd, const void *buf, size_t count);

      #include <sys/types.h>
      #include <sys/socket.h>

      ssize_t send(int sockfd, const void *buf, size_t len, int flags);
      ssize_t recv(int sockfd, void *buf, size_t len, int flags);

      ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,                      const struct sockaddr *dest_addr, socklen_t addrlen);
      ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,                        struct sockaddr *src_addr, socklen_t *addrlen);

      ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
      ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的字節數,若是返回的值是0表示已經讀到文件的結束了,小於0表示出現了錯誤。若是錯誤爲EINTR說明讀是由中斷引發的,若是是ECONNREST表示網絡鏈接出了問題。

write函數將buf中的nbytes字節內容寫入文件描述符fd.成功時返回寫的字節數。失敗時返回-1,並設置errno變量。 在網絡程序中,當咱們向套接字文件描述符寫時有倆種可能。1)write的返回值大於0,表示寫了部分或者是所有的數據。2)返回的值小於0,此時出現了錯誤。咱們要根據錯誤類型來處理。若是錯誤爲EINTR表示在寫的時候出現了中斷錯誤。若是爲EPIPE表示網絡鏈接出現了問題(對方已經關閉了鏈接)。

其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。

3.六、close()函數

在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的socket描述字,比如操做完打開的文件要調用fclose關閉打開的文件。

#include <unistd.h>int close(int fd);

close一個TCP socket的缺省行爲時把該socket標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲read或write的第一個參數。

注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。

四、socket中TCP的三次握手創建鏈接詳解

咱們知道tcp創建鏈接要進行「三次握手」,即交換三個分組。大體流程以下:

  • 客戶端向服務器發送一個SYN J

  • 服務器向客戶端響應一個SYN K,並對SYN J進行確認ACK J+1

  • 客戶端再想服務器發一個確認ACK K+1

只有就完了三次握手,可是這個三次握手發生在socket的那幾個函數中呢?請看下圖:

image

從圖中能夠看出,當客戶端調用connect時,觸發了鏈接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到鏈接請求,即收到SYN J包,調用accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1以後,這時connect返回,並對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,鏈接創建。

總結:客戶端的connect在三次握手的第二個次返回,而服務器端的accept在三次握手的第三次返回

五、socket中TCP的四次握手釋放鏈接詳解

上面介紹了socket中TCP的三次握手創建過程,及其涉及的socket函數。如今咱們介紹socket中的四次握手釋放鏈接的過程,請看下圖:

image

                                  圖二、socket中發送的TCP四次握手

圖示過程以下:

  • 某個應用進程首先調用close主動關閉鏈接,這時TCP發送一個FIN M;

  • 另外一端接收到FIN M以後,執行被動關閉,對這個FIN進行確認。它的接收也做爲文件結束符傳遞給應用進程,由於FIN的接收意味着應用進程在相應的鏈接上再也接收不到額外數據;

  • 一段時間以後,接收到文件結束符的應用進程調用close關閉它的socket。這致使它的TCP也發送一個FIN N;

  • 接收到這個FIN的源發送端TCP對它進行確認。

這樣每一個方向上都有一個FIN和ACK。

六、一個例子(實踐一下)

服務端(Server):

#include<winsock2.h>
#include<stdio.h>
#pragmacomment(lib,"ws2_32.lib")
voidmain()
{
WSADATAwsaData;
SOCKETsockServer;
SOCKADDR_INaddrServer;
SOCKETsockClient;
SOCKADDR_INaddrClient;
WSAStartup(MAKEWORD(2,2),&wsaData);
sockServer=socket(AF_INET,SOCK_STREAM,0);
addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//INADDR_ANY表示任何IP
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(6000);//綁定端口6000
bind(sockServer,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));
 
//Listen監聽端
listen(sockServer,5);//5爲等待鏈接數目
printf("服務器已啓動:\n監聽中...\n");
intlen=sizeof(SOCKADDR);
charsendBuf[100];//發送至客戶端的字符串
charrecvBuf[100];//接受客戶端返回的字符串
 
//會阻塞進程,直到有客戶端鏈接上來爲止
sockClient=accept(sockServer,(SOCKADDR*)&addrClient,&len);
//接收並打印客戶端數據
recv(sockClient,recvBuf,100,0);
printf("%s\n",recvBuf);
 
//關閉socket
closesocket(sockClient);
WSACleanup();}

客戶端 (Client):

#include<winsock2.h>
#include<stdio.h>
#pragmacomment(lib,"ws2_32.lib")
voidmain()
{
WSADATAwsaData;
SOCKETsockClient;//客戶端Socket
SOCKADDR_INaddrServer;//服務端地址
WSAStartup(MAKEWORD(2,2),&wsaData);
//新建客戶端socket
sockClient=socket(AF_INET,SOCK_STREAM,0);
//定義要鏈接的服務端地址
addrServer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//目標IP(127.0.0.1是回送地址)
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons(6000);//鏈接端口6000
//鏈接到服務端
connect(sockClient,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));
//發送數據
charmessage[20]="HelloSocket!";
send(sockClient,message,strlen(message)+1,0);
//關閉socket
closesocket(sockClient);
WSACleanup();}

1. 客戶端程序
  import java.io.*;
  import java.net.*;
  public class TalkClient {
    public static void main(String args[]) {
      try{
        Socket socket=new Socket("127.0.0.1",4700);
        //向本機的4700端口發出客戶請求
        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
        //由系統標準輸入設備構造BufferedReader對象
        PrintWriter os=new PrintWriter(socket.getOutputStream());
        //由Socket對象獲得輸出流,並構造PrintWriter對象
        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
        //由Socket對象獲得輸入流,並構造相應的BufferedReader對象
        String readline;
        readline=sin.readLine(); //從系統標準輸入讀入一字符串
        while(!readline.equals("bye")){
        //若從標準輸入讀入的字符串爲 "bye"則中止循環
          os.println(readline);
          //將從系統標準輸入讀入的字符串輸出到Server
          os.flush();
          //刷新輸出流,使Server立刻收到該字符串
          System.out.println("Client:"+readline);
          //在系統標準輸出上打印讀入的字符串
          System.out.println("Server:"+is.readLine());
          //從Server讀入一字符串,並打印到標準輸出上
          readline=sin.readLine(); //從系統標準輸入讀入一字符串
        } //繼續循環
        os.close(); //關閉Socket輸出流
        is.close(); //關閉Socket輸入流
        socket.close(); //關閉Socket
      }catch(Exception e) {
        System.out.println("Error"+e); //出錯,則打印出錯信息
      }
  }
}
 2. 服務器端程序
  import java.io.*;
  import java.net.*;
  import java.applet.Applet;
  public class TalkServer{
    public static void main(String args[]) {
      try{
        ServerSocket server=null;
        try{
          server=new ServerSocket(4700);
        //建立一個ServerSocket在端口4700監聽客戶請求
        }catch(Exception e) {
          System.out.println("can not listen to:"+e);
        //出錯,打印出錯信息
        }
        Socket socket=null;
        try{
          socket=server.accept();
          //使用accept()阻塞等待客戶請求,有客戶
          //請求到來則產生一個Socket對象,並繼續執行
        }catch(Exception e) {
          System.out.println("Error."+e);
          //出錯,打印出錯信息
        }
        String line;
        BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
         //由Socket對象獲得輸入流,並構造相應的BufferedReader對象
        PrintWriter os=newPrintWriter(socket.getOutputStream());
         //由Socket對象獲得輸出流,並構造PrintWriter對象
        BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
         //由系統標準輸入設備構造BufferedReader對象
        System.out.println("Client:"+is.readLine());
        //在標準輸出上打印從客戶端讀入的字符串
        line=sin.readLine();
        //從標準輸入讀入一字符串
        while(!line.equals("bye")){
        //若是該字符串爲 "bye",則中止循環
          os.println(line);
          //向客戶端輸出該字符串
          os.flush();
          //刷新輸出流,使Client立刻收到該字符串
          System.out.println("Server:"+line);
          //在系統標準輸出上打印讀入的字符串
          System.out.println("Client:"+is.readLine());
          //從Client讀入一字符串,並打印到標準輸出上
          line=sin.readLine();
          //從系統標準輸入讀入一字符串
        }  //繼續循環
        os.close(); //關閉Socket輸出流
        is.close(); //關閉Socket輸入流
        socket.close(); //關閉Socket
        server.close(); //關閉ServerSocket
      }catch(Exception e){
        System.out.println("Error:"+e);
        //出錯,打印出錯信息
      }
    }
  }
相關文章
相關標籤/搜索