Socket通訊原理

1、Socket通訊簡介

Android與服務器的通訊方式主要有兩種:html

  • Http通訊
  • Socket通訊

二者的最大差別在於:java

Http鏈接使用的是「請求-響應方式」,即在請求時創建鏈接通道,當客戶端向服務器發送請求後,服務端才能向客戶端返回數據android

Socket通訊則是在雙方創建鏈接後,能夠直接進行數據的傳輸,在鏈接時可實現信息的主動推送,而不須要每次由客戶端向服務器發送請求數組

那麼,什麼是socket?bash

socket又稱套接字,在程序內部提供了與外界通訊的端口,即端口通訊。服務器

經過創建socket鏈接,可爲通訊雙方的數據傳輸提供通道。socket的主要特色數據丟失率低使用簡單且易於移植markdown


一、什麼是Socket

socket是一種抽象層,應用程序經過它來發送和接受數據,使用Socket能夠將應用程序添加到網絡中,與處於同一網絡中的其餘應用程序進行通訊網絡

簡單來講,Socket提供了程序內部與外界通訊的端口併爲通訊雙方提供數據傳輸通道app

二、Socket分類

根據不一樣的底層協議,Socket的實現是多樣化的。在這主要介紹TCP/IP協議簇當中主要的Socket類型爲流套接字streamsocket)和數據報套接字datagramsocket)。socket

流套接字TCP做爲其端對端協議,提供了一個可信賴的字節流服務

數據報嵌套字使用UDP協議,提供數據打包發送數據


2、Socket基本通訊模型


一、TCP通訊模型


二、UDP通訊模型



3、Socket基本實現原理

一、基於TCP協議的Socket

服務端首先聲明一個ServerSocket對象而且指定端口號,而後調用Serversocket的accept()方法接受客戶端的數據。

accept()方法在沒有數據進行接受時處於堵塞狀態。(Socket socket = serversocket.accept()),一旦接受數據,經過inputstream讀取接受的數據。

客戶端建立一個Socket對象,執行服務器端的ip地址和端口號(Socket socket = new Socket("172.168.10.108", 8080);),經過inputstream讀取數據,獲取服務器發出的數據(OutputStream outputstream = socket.getOutputStream();),最後將要發送的數據寫入到outputstream便可進行TCP協議的socket數據傳輸


二、基於UDP協議的數據傳輸

服務器端首先建立一個DatagramSocket對象,而且指定監聽端口。接下來建立一個空的DatagramSocket對象用於接收數據(byte data[] = new byte[1024]; DatagramSocket packet = new DatagramSocket(data, data.length);),使用DatagramSocketreceive()方法接受客戶端發送的數據,receive()serversocketaccept()方法相似,在沒有數據進行接受時處於堵塞狀態。

客戶端也建立個DatagramSocket對象,而且指定監聽的端口。接下來建立一個InetAddress對象,這個對象相似於一個網絡的發送地址(InetAddress serveraddress = InetAddress.getByName("172.168.1.120"))。定義要發送的一個字符串,建立一個DatagramPacket對象,並指定要將該數據包發送到網絡對應的那個地址和端口號,最後使用DatagramSocket的對象的send()發送數據。

String str = "hello"; byte data[] = str.getByte(); DatagramPacket packet = new DatagramPacket(data, data.length, serveraddress, 4567); socket.send(packet);


4、android實現socket簡單通訊

一、使用TCP協議通訊

  • android端實現:

protected void connectServerWithTCPSocket() {

        Socket socket;
        try {// 建立一個Socket對象,並指定服務端的IP及端口號
            socket = new Socket("192.168.1.32", 1989);
            // 建立一個InputStream用戶讀取要發送的文件。
            InputStream inputStream = new FileInputStream("e://a.txt");
            // 獲取Socket的OutputStream對象用於發送數據。
            OutputStream outputStream = socket.getOutputStream();
            // 建立一個byte類型的buffer字節數組,用於存放讀取的本地文件
            byte buffer[] = new byte[4 * 1024];
            int temp = 0;
            // 循環讀取文件
            while ((temp = inputStream.read(buffer)) != -1) {
                // 把數據寫入到OuputStream對象中
                outputStream.write(buffer, 0, temp);
            }
            // 發送讀取的數據到服務端
            outputStream.flush();

            /** 或建立一個報文,使用BufferedWriter寫入,看你的需求 **/
//            String socketData = "[2143213;21343fjks;213]";
//            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
//                    socket.getOutputStream()));
//            writer.write(socketData.replace("\n", " ") + "\n");
//            writer.flush();
            /************************************************/
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
複製代碼

  • 服務器端簡單實現

public void ServerReceviedByTcp() {
        // 聲明一個ServerSocket對象
        ServerSocket serverSocket = null;
        try {
            // 建立一個ServerSocket對象,並讓這個Socket在1989端口監聽
            serverSocket = new ServerSocket(1989);
            // 調用ServerSocket的accept()方法,接受客戶端所發送的請求,
            // 若是客戶端沒有發送數據,那麼該線程就停滯不繼續
            Socket socket = serverSocket.accept();
            // 從Socket當中獲得InputStream對象
            InputStream inputStream = socket.getInputStream();
            byte buffer[] = new byte[1024 * 4];
            int temp = 0;
            // 從InputStream當中讀取客戶端所發送的數據
            while ((temp = inputStream.read(buffer)) != -1) {
                System.out.println(new String(buffer, 0, temp));
            }
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製代碼


二、使用UDP協議通訊

  • 客戶端發送數據實現:

protected void connectServerWithUDPSocket() {
        
        DatagramSocket socket;
        try {
            //建立DatagramSocket對象並指定一個端口號,注意,若是客戶端須要接收服務器的返回數據,
            //還須要使用這個端口號來receive,因此必定要記住
            socket = new DatagramSocket(1985);
            //使用InetAddress(Inet4Address).getByName把IP地址轉換爲網絡地址  
            InetAddress serverAddress = InetAddress.getByName("192.168.1.32");
            //Inet4Address serverAddress = (Inet4Address) Inet4Address.getByName("192.168.1.32");  
            String str = "[2143213;21343fjks;213]";//設置要發送的報文  
            byte data[] = str.getBytes();//把字符串str字符串轉換爲字節數組  
            //建立一個DatagramPacket對象,用於發送數據。  
            //參數一:要發送的數據   參數二:數據的長度  
            //參數三:服務端的網絡地址  參數四:服務器端端口號 
            DatagramPacket packet = new DatagramPacket(data, data.length ,serverAddress ,10025);  
            socket.send(packet);//把數據發送到服務端。  
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }  
    }
複製代碼

  • 客戶端接收服務器返回的數據

public void ReceiveServerSocketData() {
        DatagramSocket socket;
        try {
            //實例化的端口號要和發送時的socket一致,不然收不到data
            socket = new DatagramSocket(1985);
            byte data[] = new byte[4 * 1024];
            //參數一:要接受的data 參數二:data的長度
            DatagramPacket packet = new DatagramPacket(data, data.length);
            socket.receive(packet);
            //把接收到的data轉換爲String字符串
            String result = new String(packet.getData(), packet.getOffset(),
                    packet.getLength());
            socket.close();//不使用了記得要關閉
            System.out.println("the number of reveived Socket is :" + flag
                    + "udpData:" + result);
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
複製代碼

  • 服務器接收客戶端實現:

public void ServerReceviedByUdp(){
        //建立一個DatagramSocket對象,並指定監聽端口。(UDP使用DatagramSocket)  
        DatagramSocket socket;
        try {
            socket = new DatagramSocket(10025);
            //建立一個byte類型的數組,用於存放接收到得數據  
            byte data[] = new byte[4*1024];  
            //建立一個DatagramPacket對象,並指定DatagramPacket對象的大小  
            DatagramPacket packet = new DatagramPacket(data,data.length);  
            //讀取接收到得數據  
            socket.receive(packet);  
            //把客戶端發送的數據轉換爲字符串。  
            //使用三個參數的String方法。參數一:數據包 參數二:起始位置 參數三:數據包長  
            String result = new String(packet.getData(),packet.getOffset() ,packet.getLength());  
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }  
    }
複製代碼


5、總結

使用UDP方式,android端和服務器端接收能夠看出,其實android端和服務器端的發送和接受截然不同,只要端口號正確,相互通訊就沒有問題,TCP使用的是的方式發送UDP是以的形式發送


補充

一、ServerSocket.accept()方法底層源碼

查看這部分代碼主要是爲了查看accept()底層源碼實現阻塞等待的原理

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

accept()方法中調用implAccept()方法

protected final void implAccept(Socket s) throws IOException {
        SocketImpl si = null;
        try {
            if (s.impl == null)
              s.setImpl();
            else {
                s.impl.reset();
            }
            si = s.impl;
            s.impl = null;
            si.address = new InetAddress();
            si.fd = new FileDescriptor();

            //核心代碼
            getImpl().accept(si);
            ......
        } catch (IOException e) {
            ......
        }
        s.impl = si;
        s.postAccept();
    }
複製代碼

然後調用PlainSocketImpl類中的accept()方法

protected synchronized void accept(SocketImpl s) throws IOException {
        if (s instanceof PlainSocketImpl) {
            // pass in the real impl not the wrapper.
            SocketImpl delegate = ((PlainSocketImpl)s).impl;
            delegate.address = new InetAddress();
            delegate.fd = new FileDescriptor();

            // 對應代碼
            impl.accept(delegate);
            // set fd to delegate's fd to be compatible with older releases s.fd = delegate.fd; } else { // 對應代碼 impl.accept(s); } } 複製代碼

此處再調用抽象類abstracPlainSocketImpl類中的accept()方法

protected void accept(SocketImpl s) throws IOException {
        acquireFD();
        try {
            socketAccept(s);
        } finally {
            releaseFD();
        }
    }
複製代碼

其中acquireFD()方法的代碼以下:

/*
     * "Acquires" and returns the FileDescriptor for this impl
     *
     * A corresponding releaseFD is required to "release" the
     * FileDescriptor.
     */
    //「獲取」並返回這個impl的文件描述符須要一個相應的releaseFD來「釋放」文件描述符。
    FileDescriptor acquireFD() {
        synchronized (fdLock) {
            fdUseCount++;
            return fd;
        }
    }
複製代碼

然後再執行socketAccept()方法

void socketAccept(SocketImpl s) throws IOException {
        int nativefd = checkAndReturnNativeFD();

        if (s == null)
            throw new NullPointerException("socket is null");

        int newfd = -1;
        InetSocketAddress[] isaa = new InetSocketAddress[1];
        //等待阻塞代碼
        if (timeout <= 0) {
            newfd = accept0(nativefd, isaa);
        } else {
            configureBlocking(nativefd, false);
            try {
                waitForNewConnection(nativefd, timeout);
                newfd = accept0(nativefd, isaa);
                if (newfd != -1) {
                    configureBlocking(newfd, true);
                }
            } finally {
                configureBlocking(nativefd, true);
            }
        }
        /* Update (SocketImpl)s' fd '*/
        fdAccess.set(s.fd, newfd);
        /* Update socketImpls remote port, address and localport */
        InetSocketAddress isa = isaa[0];
        s.port = isa.getPort();
        s.address = isa.getAddress();
        s.localport = localport;
    }
複製代碼

該部分即爲無請求時的阻塞代碼塊,逐一查看accetp0()方法、configureBlocking()方法等發現這些代碼使用native代碼實現,提升效率。

於是具體沒有找到accept()方法的阻塞機理

我的感受,configureBlocking()方法時等待阻塞的調用的方法,而accept0()方法是響應請求的方法。

該部分純屬我的猜想,多是錯誤的,於是但願你們指教


二、OutputStream.flush()方法底層源碼

flush()方法很簡單,就是刷新此輸出流並強制寫出任何已緩衝的輸出字節

/**
     * Flushes this output stream and forces any buffered output bytes
     * to be written out. The general contract of <code>flush</code> is
     * that calling it is an indication that, if any bytes previously
     * written have been buffered by the implementation of the output
     * stream, such bytes should immediately be written to their
     * intended destination.
     * <p>
     * If the intended destination of this stream is an abstraction provided by
     * the underlying operating system, for example a file, then flushing the
     * stream guarantees only that bytes previously written to the stream are
     * passed to the operating system for writing; it does not guarantee that
     * they are actually written to a physical device such as a disk drive.
     * <p>
     * The <code>flush</code> method of <code>OutputStream</code> does nothing.
     *
     * @exception  IOException  if an I/O error occurs.
     */
    public void flush() throws IOException {
    }
複製代碼

最後附上Java測試socket,查看其底層源碼實現機制,由於一直嘗試網絡請求沒成功,只能debug一步一步查看底層實現機制。

感興趣能夠本身嘗試下,查看具體實現原理。

import java.io.*;
import java.net.*;

public class socketTest {

	public static void main(String[] args) throws UnknownHostException, IOException {
		Socket socket = setSocket();
		if(socket == null) System.out.println("socket is null");
		InputStream inputStream = new FileInputStream("d://haha.txt");
		OutputStream outputStream = socket.getOutputStream();
		byte buffer[] = new byte[4 * 1024];
		int temp = 0;
		while((temp = inputStream.read(buffer)) != -1) {
			outputStream.write(buffer, 0, temp);
		}
		outputStream.flush();
		listener();
	}
	
	public static Socket setSocket() throws IOException{
        String ip = "127.0.0.5";
        int port = 8080;
        Socket socket = new Socket();
        //設置最長等待時間
        socket.setSoTimeout(8000);
        //進行鏈接請求
        socket.connect(new InetSocketAddress(ip, port));
        return socket;
    }
    
    //服務端監聽方法
    public static void listener() throws IOException{
        //正常狀況下,此處的"9999"和客戶端中的端口號port須要相同,才能完成網絡請求,
        //由於測試不成功,可是又想查看實現原理,於是才這樣作!!!
    	ServerSocket server = new ServerSocket(9999);
        Socket socket = null;
        int i = 0;
        while(true){
            i++;
            socket = server.accept();//這也是個阻塞的方法來的
            System.out.println("有" + i + "個用戶鏈接了服務器");
            new Thread(new socketTest().new ServerDoThread(socket)).start();
        }
    }

    class ServerDoThread implements Runnable {
    	Socket socket;
    	InputStream inputStream;
    	
    	public ServerDoThread(Socket socket) {
    		this.socket = socket;
    		try {
				this.inputStream = socket.getInputStream();
			} catch (IOException e) {
				e.printStackTrace();
			}
    	}

    	@Override
    	public void run() {
    		byte buffer[] = new byte[1024 * 4];
    		int temp = 0;
    		try {
				while((temp = inputStream.read(buffer)) != -1) {
					System.out.println(new String(buffer, 0, temp));
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
    	}
    }

}
複製代碼



來源:Socket 通訊原理(Android客戶端和服務器以TCP&&UDP方式互通)

相關文章
相關標籤/搜索