Java實現TCP通信

TCP(Transmission Control Protocol),即傳輸控制協議。是一種面向連接的、可靠的、基于字節流的傳輸層通信協議。不同于UDP,TCP更像是提供一種可靠的、像管道一樣的連接。

Java中的TCP主要涉及ServerSocket和Socket兩個類。前者被認為是服務端的一個實體,用于接受連接。后者則被認為是連接的一種封裝,用于傳輸數據,類似于一個管道。

下面就來實現一下服務端與客戶端。

服務端:

public class TCPService {
    public static final String SERVICE_IP = "127.0.0.1";

    public static final int SERVICE_PORT = 10101;

    public static final char END_CHAR = '#';

    public static void main(String[] args) {
        TCPService service = new TCPService();
        //啟動服務端
        service.startService(SERVICE_IP,SERVICE_PORT);
    }

    private void startService(String serverIP, int serverPort){
        try {
            //封裝服務端地址
            InetAddress serverAddress = InetAddress.getByName(serverIP);
            //建立服務端
            try(ServerSocket service = new ServerSocket(serverPort, 10, serverAddress)){
                while (true) {
                    StringBuilder receiveMsg = new StringBuilder();
                    //接受一個連接,該方法會阻塞程序,直到一個鏈接到來
                    try(Socket connect = service.accept()){
                        //獲得輸入流
                        InputStream in = connect.getInputStream();
                        
                        //解析輸入流,遇到終止符結束,該輸入流來自客戶端
                        for (int c = in.read(); c != END_CHAR; c = in.read()) {
                            if(c ==-1)
                                break;
                            receiveMsg.append((char)c);
                        }
                        
                        //組建響應信息
                        String response = "Hello world " + receiveMsg.toString() + END_CHAR;
                        
                        //獲取輸入流,并通過向輸出流寫數據的方式發送響應
                        OutputStream out = connect.getOutputStream();
                        out.write(response.getBytes());
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}

客戶端

public class TCPClient {

    public static void main(String[] args) {
        TCPClient client = new TCPClient();
        SimpleDateFormat format = new SimpleDateFormat("hh-MM-ss");
        Scanner scanner = new Scanner(System.in);
        while(true){
            String msg = scanner.nextLine();
            if("#".equals(msg))
                break;
            //打印響應的數據
            System.out.println("send time : " + format.format(new Date()));
            System.out.println(client.sendAndReceive(TCPService.SERVICE_IP,TCPService.SERVICE_PORT,msg));
            System.out.println("receive time : " + format.format(new Date()));
        }
    }

    private String sendAndReceive(String ip, int port, String msg){
        //這里比較重要,需要給請求信息添加終止符,否則服務端會在解析數據時,一直等待
        msg = msg+TCPService.END_CHAR;
        StringBuilder receiveMsg = new StringBuilder();
        //開啟一個鏈接,需要指定地址和端口
        try (Socket client = new Socket(ip, port)){
            //向輸出流中寫入數據,傳向服務端
            OutputStream out = client.getOutputStream();
            out.write(msg.getBytes());

            //從輸入流中解析數據,輸入流來自服務端的響應
            InputStream in = client.getInputStream();
            for (int c = in.read(); c != TCPService.END_CHAR; c = in.read()) {
                if(c==-1)
                    break;
                receiveMsg.append((char)c);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return receiveMsg.toString();
    }
}

單從代碼結構的角度來看,UDP通信服務端與客戶端代碼是相似的,都是依托于DatagramPacket 對象收發信息。而TCP通信中,只有服務端有一個實體,客戶端只要借助Socket收發信息即可,發送完關閉Socket。

上面有一點需要注意,在讀輸入流時,必須做讀到流結束判斷,就是讀到-1,若沒有做判斷,在這樣情況下會出錯:若一個連接連接成功后,沒有發生任何信息,或信息中沒有結束字符,就關閉了連接,由于TCP連接是雙向的,導致另一端一直從輸入流中讀到流結束標志,很快會導致OOM,所以在讀到結束符時,要及時跳出循環。結束符只會在連接中斷時發出,而在等待輸入時,不會出現,所以不必擔心在等待響應時由于讀到該字符導致服務端或客戶端提前中斷連接。

另外Socket和ServerSocket在jdk 1.7之后都實現了AutoCloseable接口,所以可以用try-with-resources結構。之前的UDP里的DatagramPacket 也一樣

這就是一個簡單的阻塞型服務器模型,分析代碼我們可知,如果一次請求時間過長,會影響到后續請求的執行。我們可以在服務端輸出時加一個sleep,啟動兩個客戶端,分別發送消息,觀察log,服務端延遲5s,結果如下:

客戶端1:
send time : 06-04-06
Hello world 1
receive time : 06-04-11
客戶端2:
send time : 06-04-08
Hello world 2
receive time : 06-04-16

其中客戶端1先發送,客戶端2后發送,可見客戶端在等待服務器處理完客戶端1的請求后才處理客戶端2的請求

由此我們可以預見,當服務器接到一個需要長時間處理的請求時,會阻塞后續的請求,這也就是這種類型服務器容易遭到攻擊的原因。為了應對這種局面,我們可以在收到一個請求時,調用子線程去處理,服務器時刻處在接受請求的狀態。

public class TCPService1 {
    public static final String SERVICE_IP = "127.0.0.1";

    public static final int SERVICE_PORT = 10101;

    public static final char END_CHAR = '#';

    public static void main(String[] args) {
        TCPService1 service1 = new TCPService1();
        service1.startService();
    }

    private void startService(){
        try {
            InetAddress address = InetAddress.getByName(SERVICE_IP);
            Socket connect = null;
            ExecutorService pool = Executors.newFixedThreadPool(5);
            try (ServerSocket service = new ServerSocket(SERVICE_PORT,5,address)){
                while(true){
                    connect = service.accept();
                    //創建一個任務
                    ServiceTask serviceTask = new ServiceTask(connect);
                    //放入線程池等待運行
                    pool.execute(serviceTask);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(connect!=null)
                    connect.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    class ServiceTask implements Runnable{
        private Socket socket;

        ServiceTask(Socket socket){
            this.socket = socket;
        }
        @Override
        public void run() {
            try {
                StringBuilder receiveMsg = new StringBuilder();
                InputStream in = socket.getInputStream();
                for (int c = in.read(); c != END_CHAR; c = in.read()) {
                    if(c ==-1)
                        break;
                    receiveMsg.append((char)c);
                }
                String response = "Hello world " + receiveMsg.toString() + END_CHAR;
                Thread.currentThread().sleep(5000);
                OutputStream out = socket.getOutputStream();
                out.write(response.getBytes());
            }catch (Exception e){
                e.printStackTrace();
            }finally {
               if(socket!=null)
                   try {
                       socket.close();
                   } catch (IOException e) {
                       e.printStackTrace();
                   }
            }
        }
    }
}

在這個服務器中,我們采用了線程池的做法,每到一個請求,我們就向線程池中添加一個任務。實際運行情況如下:

客戶端1
send time : 03-04-59
Hello world 1
receive time : 03-04-04
客戶端2
send time : 03-04-01
Hello world 2
receive time : 03-04-06

可見每個客戶端能在發送信息后得到響應,不必排隊。但是這種類型的服務器并不能保證實時響應,當請求數過多時,服務器資源會被耗盡,或者服務器有最大線程數有限制,多余的請求依然會被阻塞。

第一二種服務器模型中,我們在讀取流的時候加入了自定義的結束符,同時采用for循環,但是一次從輸入流中讀一個數據,效率比較低,我們可以采用緩沖區的方法,但是這種方法不能判斷自定義的結束符,只能判斷流結束,所以要及時關閉流,如客戶端發完數據后關閉輸出流:

OutputStream out = client.getOutputStream();
out.write(msg.getBytes());
client.shutdownOutput();
InputStream in = client.getInputStream();
int len;
byte[] buffer = new byte[1024];
while((len = in.read(buffer))!=-1)
       receiveMsg.append(new String(buffer,0,len));

由于TCP通信是雙向的,所以可以單獨關閉一端,但是不能直接關閉輸入或輸出流,這樣會將整個Socket關閉。

最后簡單介紹一下涉及的幾個類

1. Socket
該類的構造方法有許多,但目的都只有一個,就是創建一個指向服務端的鏈接,用于收發數據。

Socket(InetAddress address, int port)  //通過InetAddress指定服務端地址
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) //指定服務器地址與端口還指定客戶端的地址和端口
Socket(String host, int port) //通過字符串指定服務端地址
Socket(String host, int port, InetAddress localAddr, int localPort)

可以發現上面構造分為兩類,一類只指定服務端的地址和端口。另一類在第一類的基礎上還指定了客戶端的地址和端口號。其實第一類是默認本機地址作為客戶端地址,同時隨機選一個可用端口作為客戶端端口而已。

常用方法:

OutputStream getOutputStream()//獲得輸出流
InputStream getInputStream() //獲得輸入流
void connect(SocketAddress endpoint) //鏈接到服務端
void connect(SocketAddress endpoint, int timeout)//鏈接到服務端,并指定超時時間
//幾個獲取本地端相關信息的方法
InetAddress getLocalAddress()
int getLocalPort()
SocketAddress getLocalSocketAddress()
//幾個獲得遠端相關信息的方法
int getPort()
InetAddress getInetAddress()
SocketAddress getRemoteSocketAddress()

2. ServerSocket
構造方法

ServerSocket(int port) //指定服務器端口,默認tcp隊列為50,監聽所有本機地址的鏈接,用于多網卡設備
ServerSocket(int port, int backlog) //指定端口和隊列長度
ServerSocket(int port, int backlog, InetAddress bindAddr) //指定所有信息

常用方法:

Socket accept() //從隊列中取一個請求,阻塞型方法
void bind(SocketAddress endpoint) //對于無參的構造進行地址與端口綁定
void bind(SocketAddress endpoint, int backlog)
//獲取本地的一些信息
InetAddress getInetAddress()
int getLocalPort()
SocketAddress getLocalSocketAddress()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容