Socket的使用

要運行下面的全部項目,請關閉防火牆

基於TCP/IP協議的Socket使用

 

android 客戶端向服務端發送信息

服務端代碼:html

package com.example.admin.server;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class MyClass {
public static void main(String[] arugs) throws IOException {
try {
call();
} catch (IOException e) {
System.out.print("io failed"); }

}

public static void call() throws IOException {
InputStream inputStream = null;
Socket socket=null;
ServerSocket serverSocket=null;

try {
serverSocket = new ServerSocket(5050);
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
System.out.println("服務端ip地址:" + ip);
socket = serverSocket.accept();
inputStream = socket.getInputStream();
StringBuilder builder = new StringBuilder();
byte[] bytes = new byte[1024];
int length;
while ((length = inputStream.read(bytes)) != -1) {
builder.append(new String(bytes, 0, length, "UTF-8"));
}
System.out.println(builder.toString());
} catch (UnknownHostException e) {
System.out.print("null localHost");
} finally {
if (inputStream != null) {
inputStream.close();
}if (socket!=null){
socket.close();
}if (serverSocket!=null){
serverSocket.close(); }

}
}
}

控制檯輸出: 服務端ip地址:xxx.xxx.x.x


客戶端代碼:
首先添加網絡權限
<uses-permission android:name="android.permission.INTERNET"/>

package com.example.admin.socket;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;


public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button=findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
super.run();
try {
call();
}catch (IOException e){
Log.e("io","io close failed"); }

}
}.start();

}
});
}

public void call()throws IOException{
Socket socket=null;
OutputStream out=null;

try {
socket = new Socket("xxx.xxx.x.x", 5050);
out = socket.getOutputStream();
out.write("Hello World".getBytes( "UTF-8"));
}catch (UnknownHostException e){
Log.e("socket", "create socket failed");
} catch (IOException e) {
Log.e("io","outputstream problem ");

}finally {
if (out!=null)
out.close();
}
if (socket!=null){
socket.close();
}
}
}


點擊按鈕後,控制檯輸出:服務端ip地址:xxx.xxx.x.x
Hello World




android客戶端向服務端發送消息,並接受消息

服務端代碼:java

package com.example.admin.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;

public class MyClass {
public static void main(String[] arugs) throws IOException {
try {
call();
} catch (IOException e) {
System.out.print("io failed"); }

}

public static void call() throws IOException {
InputStream inputStream = null;
OutputStream outputStream=null;
Socket socket=null;
ServerSocket serverSocket=null;

try {
serverSocket = new ServerSocket(5050);
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
System.out.println("服務端ip地址:" + ip);
socket = serverSocket.accept();
inputStream = socket.getInputStream();
StringBuilder builder = new StringBuilder();
byte[] bytes = new byte[1024];
int length;
while ((length = inputStream.read(bytes)) != -1) {
builder.append(new String(bytes, 0, length, "UTF-8"));
}
System.out.println(builder.toString());
outputStream=socket.getOutputStream();
outputStream.write("Welcome to the new world!".getBytes("UTF-8"));
} catch (UnknownHostException e) {
System.out.print("null localHost");
} finally {
if (inputStream != null) {
inputStream.close();
}if (outputStream!=null){
outputStream.close();
}
if (socket!=null){
socket.close();
}if (serverSocket!=null){
serverSocket.close();
}

}
}
}
客戶端代碼:
package com.example.admin.socket;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;

public class MainActivity extends AppCompatActivity {
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView=findViewById(R.id.textview);
Button button=findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
super.run();
try {
call();
}catch (IOException e){
Log.e("io","io close failed");
}

}
}.start();

}

});

}



public void call()throws IOException{
final StringBuilder builder=new StringBuilder();
Socket socket=null;
OutputStream out=null;
InputStream in=null;

try {
socket = new Socket("xxx.xxx.x.x", 5050);
out = socket.getOutputStream();
out.write("Hello World".getBytes( "UTF-8"));
//經過shutdownOutput高速服務器已經發送完數據,後續只能接受數據
socket.shutdownOutput();
in=socket.getInputStream();
byte[] bytes=new byte[1024];
int length;
while ((length=in.read(bytes))!=-1){
builder.append(new String(bytes,0,length,"UTF-8"));
}
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(builder.toString());

}
});
}catch (UnknownHostException e){
Log.e("socket", "create socket failed");
} catch (IOException e) {
Log.e("io","outputstream problem ");

}finally {
if (out!=null){
out.close();
}if (in!=null){
in.close();
}
if (socket!=null){
socket.close();
}
}

}


}
android客戶端顯示 :Welcome to the new world!

如何告知對方已發送完命令

  其實這個問題仍是比較重要的,正常來講,客戶端打開一個輸出流,若是不作約定,也不關閉它,那麼服務端永遠不知道客戶端是否發送完消息,那麼服務端會一直等待下去,直到讀取超時。因此怎麼告知服務端已經發送完消息就顯得特別重要。android

 

1.經過Socket關閉

   當Socket關閉的時候,服務端就會收到響應的關閉信號,那麼服務端也就知道流已經關閉了,這個時候讀取操做完成,就能夠繼續後續工做。算法

     可是這種方式有一些缺點編程

  • 客戶端Socket關閉後,將不能接受服務端發送的消息,也不能再次發送消息
  • 若是客戶端想再次發送消息,須要重現建立Socket鏈接

 

 

2.經過Socket關閉輸出流的方式

  調用Socket的shutdownOutput()方法,底層會告知服務端我這邊已經寫完了,那麼服務端收到消息後,就能知道已經讀取完消息,若是服務端有要返回給客戶的消息那麼就能夠經過服務端的輸出流發送給客戶端,若是沒有,直接關閉Socket。數組

    這種方式經過關閉客戶端的輸出流,告知服務端已經寫完了,雖然能夠讀到服務端發送的消息,可是仍是有一點點缺點:緩存

  • 不能再次發送消息給服務端,若是再次發送,須要從新創建Socket鏈接

  這個缺點,在訪問頻率比較高的狀況下將是一個須要優化的地方。安全

 

 

3.經過約定符號

  這種方式的用法,就是雙方約定一個字符或者一個短語,來當作消息發送完成的標識,一般這麼作就須要改造讀取方法。服務器

  假如約定單端的一行爲end,表明發送完成,例以下面的消息,end則表明消息發送完成網絡


static final String end="bye";

Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = read.readLine()) != null && "end".equals(line)) { //注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(line); }

 能夠看見,服務端不只判斷是否讀到了流的末尾,還判斷了是否讀到了約定的末尾。

  這麼作的優缺點以下:

  • 優勢:不須要關閉流,當發送完一條命令(消息)後能夠再次發送新的命令(消息)
  • 缺點:須要額外的約定結束標誌,太簡單的容易出如今要發送的消息中,誤被結束,太複雜的很差處理,還佔帶寬

 

4 .經過指定長度

  • 1個字節:最大256,表示256B
  • 2個字節:最大65536,表示64K
  • 3個字節:最大16777216,表示16M
  • 4個字節:最大4294967296,表示4G

這個時候是否是很糾結,最大的固然是最保險的,可是真的有必要選擇最大的嗎,其實若是你稍微瞭解一點UTF-8的編碼方式,那麼你就應該能想到爲何必定要固定表示長度字節的長度呢,咱們可使用變長方式來表示長度的表示,好比:
  • 第一個字節首位爲0:即0XXXXXXX,表示長度就一個字節,最大128,表示128B
  • 第一個字節首位爲110,那麼附帶後面一個字節表示長度:即110XXXXX 10XXXXXX,最大2048,表示2K
  • 第一個字節首位爲1110,那麼附帶後面二個字節表示長度:即110XXXXX 10XXXXXX 10XXXXXX,最大131072,表示128K
  • 依次類推

若是用做命名發送,兩個字節就夠了,若是還不放心4個字節基本就能知足你的全部要求,下面的例子咱們將採用2個字節表示長度,目的只是給你一種思路,讓你知道有這種方式來獲取消息的結尾:
package yiwangzhibujian.waitreceive2; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws Exception { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes; // 由於能夠複用Socket且能判斷長度,因此能夠一個Socket用到底 while (true) { // 首先讀取兩個字節表示的長度 int first = inputStream.read(); //若是讀取的值爲-1 說明到了流的末尾,Socket已經被關閉了,此時將不能再去讀取 if(first==-1){ break; } int second = inputStream.read(); int length = (first << 8) + second; // 而後構造一個指定長的byte數組 bytes = new byte[length]; // 而後讀取指定長度的消息便可 inputStream.read(bytes); System.out.println("get message from client: " + new String(bytes, "UTF-8")); } inputStream.close(); socket.close(); server.close(); } }

 此處的讀取步驟爲,先讀取兩個字節的長度,而後讀取消息,客戶端爲:
package yiwangzhibujian.waitreceive2; import java.io.OutputStream; import java.net.Socket; public class SocketClient { public static void main(String args[]) throws Exception { // 要鏈接的服務端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(host, port); // 創建鏈接後得到輸出流 OutputStream outputStream = socket.getOutputStream(); String message = "你好 yiwangzhibujian"; //首先須要計算得知消息的長度 byte[] sendBytes = message.getBytes("UTF-8"); //而後將消息的長度優先發送出去 outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); //而後將消息再次發送出去 outputStream.write(sendBytes); outputStream.flush(); //==========此處重複發送一次,實際項目中爲多個命名,此處只爲展現用法 message = "第二條消息"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.flush(); //==========此處重複發送一次,實際項目中爲多個命名,此處只爲展現用法 message = "the third message!"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.close(); socket.close(); } }

 客戶端要多作的是,在發送消息以前先把消息的長度發送過去。

  這種事先約定好長度的作法解決了以前提到的種種問題,Redis的Java客戶端Jedis就是用這種方式實現的這種方式的缺點:

  • 暫時還沒發現

  固然若是是須要服務器返回結果,那麼也依然使用這種方式,服務端也是先發送結果的長度,而後客戶端進行讀取。固然如今流行的就是,長度+類型+數據模式的傳輸方式。

 

服務端併發處理能力

在上面的例子中,服務端僅僅只是接受了一個Socket請求,並處理了它,而後就結束了,可是在實際開發中,一個Socket服務每每須要服務大量的Socket請求,那麼就不能再服務完一個Socket的時候就關閉了,這時候能夠採用循環接受請求並處理的邏輯:

新手寫法,有嚴重問題,不推薦:

package yiwangzhibujian.multiserver;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {
  public static void main(String args[]) throws IOException {
    // 監聽指定的端口
    int port = 55533;
    ServerSocket server = new ServerSocket(port);
    // server將一直等待鏈接的到來
    System.out.println("server將一直等待鏈接的到來");
    
    while(true){
      Socket socket = server.accept();
      // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取
      InputStream inputStream = socket.getInputStream();
      byte[] bytes = new byte[1024];
      int len;
      StringBuilder sb = new StringBuilder();
      while ((len = inputStream.read(bytes)) != -1) {
        // 注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8
        sb.append(new String(bytes, 0, len, "UTF-8"));
      }
      System.out.println("get message from client: " + sb);
      inputStream.close();
      socket.close();
    }
    
  }
}


服務端每次接收到客戶端的請求後,都會建立一個新的線程去處理,而jvm的線程數量過可能是,服務端處理速度會變慢,並且當一個請求的處理比較耗時的時候,後面的請求將被阻塞。

  咱們能夠用線程池解決。

      線程池的優勢:

  • 線程複用,建立線程耗時,回收線程慢

  • 防止短期內高併發,指定線程池大小,超過數量將等待,方式短期建立大量線程致使資源耗盡,服務掛掉


服務端代碼以下:


package
com.example.admin.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.net.Socket;

public class thread extends Thread {

private Socket socket;
public thread(Socket socket){
this.socket=socket;
}


@Override
public void run() {
try {
System.out.println("當前線程:"+Thread.currentThread().getName());
InputStream inputStream = socket.getInputStream();
byte[] bytes;
while (true) {
int first = inputStream.read();
if (first == -1) {
break;
}
int second = inputStream.read();
int length = (first >> 8) + second;
bytes = new byte[length];
inputStream.read(bytes);
System.out.println(new String(bytes, "UTF-8"));
}

OutputStream outputStream = socket.getOutputStream();
outputStream.write("Welcome to the new world!".getBytes("UTF-8"));
inputStream.close();
outputStream.close();
} catch (IOException e) {
System.out.print("null localHost");
}
}
}



package com.example.admin.server;


import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;



public class MyClass {

public static void main(String[] arugs) throws IOException {
        //線程過多,渣電腦當心卡死...
ExecutorService service = Executors.newFixedThreadPool(3);
ServerSocket serverSocket = new ServerSocket(5050);
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
System.out.println("服務端ip地址:" + ip);
Socket socket = null;

while (true) {
socket = serverSocket.accept();
service.execute(new thread(socket));
}
}

}
客戶端代碼同上

屢次點擊發送
控制檯輸出: 

服務端ip地址:xxx.xxx.x.x
當前線程:pool-1-thread-1
one
two
當前線程:pool-1-thread-2
one
two
當前線程:pool-1-thread-3
one
two
當前線程:pool-1-thread-1
one
two

服務端其餘屬性

  • SO_TIMEOUT:表示等待客戶鏈接的超時時間。通常不設置,會持續等待。
  • SO_REUSEADDR:表示是否容許重用服務器所綁定的地址。通常不設置,經個人測試不必,下面會進行詳解。
  • SO_RCVBUF:表示接收數據的緩衝區的大小。通常不設置,用系統默認就能夠了。

       體詳細的解釋能夠參照下面。

Socket的其它知識

  其實若是常常看有關網絡編程的源碼的話,就會發現Socket仍是有不少設置的,能夠學着用,可是仍是要有一些基本的瞭解比較好。下面就對Socket的Java API中涉及到的進行簡單講解。首先呢Socket有哪些能夠設置的選項,其實在SocketOptions接口中已經都列出來了:

  • int TCP_NODELAY = 0x0001:對此鏈接禁用 Nagle 算法。
  • int SO_BINDADDR = 0x000F:此選項爲 TCP 或 UDP 套接字在 IP 地址頭中設置服務類型或流量類字段。
  • int SO_REUSEADDR = 0x04:設置套接字的 SO_REUSEADDR。
  • int SO_BROADCAST = 0x0020:此選項啓用和禁用發送廣播消息的處理能力。
  • int IP_MULTICAST_IF = 0x10:設置用於發送多播包的傳出接口。
  • int IP_MULTICAST_IF2 = 0x1f:設置用於發送多播包的傳出接口。
  • int IP_MULTICAST_LOOP = 0x12:此選項啓用或禁用多播數據報的本地回送。
  • int IP_TOS = 0x3:此選項爲 TCP 或 UDP 套接字在 IP 地址頭中設置服務類型或流量類字段。
  • int SO_LINGER = 0x0080:指定關閉時逗留的超時值。
  • int SO_TIMEOUT = 0x1006:設置阻塞 Socket 操做的超時值: ServerSocket.accept(); SocketInputStream.read(); DatagramSocket.receive(); 選項必須在進入阻塞操做前設置才能生效。
  • int SO_SNDBUF = 0x1001:設置傳出網絡 I/O 的平臺所使用的基礎緩衝區大小的提示。
  • int SO_RCVBUF = 0x1002:設置傳入網絡 I/O 的平臺所使用基礎緩衝區的大小的提示。
  • int SO_KEEPALIVE = 0x0008:爲 TCP 套接字設置 keepalive 選項時
  • int SO_OOBINLINE = 0x1003:置 OOBINLINE 選項時,在套接字上接收的全部 TCP 緊急數據都將經過套接字輸入流接收。

 

客戶端綁定端口

  服務端綁定端口是能夠理解的,由於要監聽指定的端口,可是客戶端爲何要綁定端口,說實話我以爲這麼作的人有點2,或許有的網絡安全策略配置了端口訪出,使用戶只能使用指定的端口,那麼這樣的配置也是挺2的,直接說就能夠不要留面子。

  固然首先要理解的是,若是沒有指定端口的話,Socket會自動選取一個能夠用的端口,不用瞎操心的。

  可是你非得指定一個端口也是能夠的,作法以下,這時候就不能用Socket的構造方法了,要一步一步來:

 
// 要鏈接的服務端IP地址和端口 String host = "localhost"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(); socket.bind(new InetSocketAddress(55534)); socket.connect(new InetSocketAddress(host, port));
這樣作就能夠了,可是當這個程序執行完成之後,再次執行就會報,端口占用異常:

java.net.BindException: Address already in use: connect

明明上一個Socket已經關閉了,爲何再次使用還會說已經被佔用了呢?若是你是用netstat 命令來查看端口的使用狀況:


netstat -n|findstr "55533"
TCP 127.0.0.1:55534 127.0.0.1:55533 TIME_WAIT
簡單來講,當鏈接主動關閉後,端口狀態變爲TIME_WAIT,其餘程序依然不能使用這個端口,防止服務端由於超時從新發送的確認鏈接斷開對新鏈接的程序形成影響。

  TIME_WAIT的時間通常有底層決定,通常是2分鐘,還有1分鐘和30秒的。

  因此,客戶端不要綁定端口,不要綁定端口,不要綁定端口。

 

 

讀超時SO_TIMEOUT

  讀超時這個屬性仍是比較重要的,當Socket優化到最後的時候,每每一個Socket鏈接會一直用下去,那麼當一端由於異常致使鏈接沒有關閉,另外一方是不該該持續等下去的,因此應該設置一個讀取的超時時間,當超過指定的時間後,尚未讀到數據,就假定這個鏈接無用,而後拋異常,捕獲異常後關閉鏈接就能夠了,調用方法爲:

public void setSoTimeout(int timeout) throws SocketException

  timeout - 指定的以毫秒爲單位的超時值。設置0爲持續等待下去。建議根據網絡環境和實際生產環境選擇。

  這個選項設置的值將對如下操做有影響:

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

 

判斷Socket是否可用

  當須要判斷一個Socket是否可用的時候,不能簡簡單單判斷是否爲null,是否關閉,下面給出一個比較全面的判斷Socket是否可用的表達式,這是根據Socket自身的一些狀態進行判斷的,它的狀態有:

  • bound:是否綁定
  • closed:是否關閉
  • connected:是否鏈接
  • shutIn:是否關閉輸入流
  • shutOut:是否關閉輸出流
socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()&& !socket.isInputShutdown() && !socket.isOutputShutdown()


建議如此使用,但這只是第一步,保證Socket自身的狀態是可用的,
可是當鏈接正常建立後,上面的屬性若是不調用本方相應的方法是不會改變的,也就是說若是網絡斷開、服務器主動斷開,Java底層是不會檢測到鏈接斷開並改變Socket的狀態,
因此,真實的檢測鏈接狀態仍是得經過額外的手段,有兩種方式。

1 自定義心跳包

  雙方須要約定,什麼樣的消息屬於心跳包,什麼樣的消息屬於正常消息,假設你看了上面的內容就容易理解了,咱們定義前兩個字節爲消息的長度,那麼咱們就能夠定義第3個字節爲消息的屬性,能夠指定一位爲消息的類型,1爲心跳,0爲正常消息。那麼要作的有以下:

  • 客戶端發送心跳包
  • 服務端獲取消息判斷是不是心跳包,如果丟棄
  • 當客戶端發送心跳包失敗時,就能夠判定鏈接不可用

  具體的編碼再也不貼出,本身實現便可。

 

2經過發送緊急數據

  Socket自帶一種模式,那就是發送緊急數據,這有一個前提,那就是服務端的OOBINLINE不能設置爲true,它的默認值是false。

  OOBINLINE的true和false影響了什麼:

  • 對客戶端沒有影響
  • 對服務端,若是設置爲true,那麼服務端將會捕獲緊急數據,這會對接收數據形成混淆,須要額外判斷

  發送緊急數據經過調用Socket的方法:

    socket.sendUrgentData(0);

  發送數據任意便可,由於OOBINLINE爲false的時候,服務端會丟棄掉緊急數據。

  當發送緊急數據報錯之後,咱們就會知道鏈接不通了。

 

真得須要判斷鏈接斷開嗎

  經過上面的兩種方式已經能夠判斷出鏈接是否可用,而後咱們就能夠進行後續操做,但是請你們認真考慮下面的問題:

  1. 發送心跳成功時確認鏈接可用,當再次發送消息時能保證鏈接還可用嗎?即使中間的間隔很短
  2. 若是鏈接不可用了,你會怎麼作?從新創建鏈接再次發送數據?仍是說單單只是記錄日誌?
  3. 若是你打算從新創建鏈接,那麼發送心跳包的意義何在?爲什麼不在發送異常時再新建鏈接?

  若是你認真考慮了上面的問題,那麼你就會以爲發送心跳包徹底是沒有必要的操做,經過發送心跳包來判斷鏈接是否可用是經過捕獲異常來判斷的。那麼咱們徹底能夠在發送消息報出IO異常的時候,在異常中從新發送一次便可,這兩種方式的編碼有什麼不一樣呢,下面寫一寫僞代碼。

 

  提早檢測鏈接是否可用:

//有一個鏈接中的socket Socket socket=... //要發送的數據 String data=""; try{ //發送心跳包或者緊急數據,來檢測鏈接的可用性 }catch (Excetption e){ //打印日誌,並重連Socket socket=new Socket(host,port); } socket.write(data);
直接發送數據,出異常後從新鏈接再次發送:
//有一個鏈接中的socket Socket socket=... //要發送的數據 String data=""; try{ socket.write(data); }catch (Excetption e){ //打印日誌,並重連Socket socket=new Socket(host,port); socket.write(data); }
經過比較能夠發現兩種方式的特色,如今簡單介紹下:
  • 兩種方式都可實現鏈接斷開從新鏈接併發送
  • 提早檢測,再每次發送消息的時候都要檢測,影響效率,佔用帶寬

  但願你們認真考慮,作出本身的選擇。

 

設置端口重用SO_REUSEADDR 

 

  首先,建立Socket時,默認是禁止的,設置true有什麼做用呢,Java API中是這麼介紹的:

 

關閉 TCP 鏈接時,該鏈接可能在關閉後的一段時間內保持超時狀態(一般稱爲 TIME_WAIT 狀態或 2MSL 等待狀態)。對於使用已知套接字地址或端口的應用程序而言,若是存在處於超時狀態的鏈接(包括地址和端口),可能不能將套接字綁定到所需的 SocketAddress 上。

 

使用 bind(SocketAddress) 綁定套接字前啓用 SO_REUSEADDR 容許在上一個鏈接處於超時狀態時綁定套接字。

 

  通常是用在綁定端口的時候使用,可是通過個人測試建議以下:

 
  • 服務端綁定端口後,關閉服務端,從新啓動後不會提示端口占用
  • 客戶端綁定端口後,關閉,即使設置ReuseAddress爲true,即使能綁定端口,鏈接的時候仍是會報端口占用異常
 

設置關閉等待SO_LINGER

  Java API的介紹是:啓用/禁用具備指定逗留時間(以秒爲單位)的 SO_LINGER。最大超時值是特定於平臺的。 該設置僅影響套接字關閉。 

  你們都是這麼說的,當調用Socket的close方法後,沒有發送的數據將再也不發送,設置這個值的話,Socket會等待指定的時間發送完數據包。說實話,通過我簡單的測試,對於通常數據量來講,幾十K左右,即使直接關閉Socket的鏈接,服務端也是能夠收到數據的。

  因此對於通常應用不必設置這個值,當數據量發送過大拋出異常時,再來設置這個值也不晚。那麼到達逗留超時值時,套接字將經過 TCP RST 強制性 關閉。啓用超時值爲零的選項將當即強制關閉。若是指定的超時值大於 65,535,則其將被減小到 65,535。

  綜上所述,不建議綁定端口,也不必設置ReuseAddress,固然ReuseAddress的底層仍是和硬件有關係的,或許在你的機器上測試結果和我不同,如果如此和平臺相關性差別這麼大配置更是不建議使用了。

 

設置發送延遲策略TCP_NODELAY

 

  通常來講當客戶端想服務器發送數據的時候,會根據當前數據量來決定是否發送,若是數據量太小,那麼系統將會根據Nagle 算法(暫時還沒研究),來決定發送包的合併,也就是說發送會有延遲,這在有時候是致命的,好比說對實時性要求很高的消息發送,在線對戰遊戲等,即使數據量很小也要求當即發送,若是稍有延遲就會感受到卡頓,默認狀況下Nagle 算法是開啓的,因此若是不打算有延遲,最好關閉它。這樣一旦有數據將會當即發送而不會寫入緩衝區。

 

  可是對延遲要求不是特別高下仍是可使用的,仍是能夠提高網絡傳輸效率的。

 

設置輸出輸出緩衝區大小SO_RCVBUF/SO_SNDBUF

  • SO_SNDBUF:發送緩衝
  • SO_RCVBUF:接收緩衝

  默認都是8K,若是有須要能夠修改,經過相應的set方法。不建議修改的過小,設置過小數據傳輸將過於頻繁。太大了將會形成消息停留。

  不過我對這個通過測試後有如下結論:

  • 當數據填滿緩衝區時,必定會發送
  • 當數據沒有填滿緩衝區時也會發送,這個算法仍是上面說的Nagle 算法

 

設置保持鏈接存活SO_KEEPALIVE

  雖說當設置鏈接鏈接的讀超時爲0,即無限等待時,Socket不會被主動關閉,可是總會有莫名其妙的軟件來檢測你的鏈接是否有數據發送,長時間沒有數據傳輸的鏈接會被它們關閉掉。

  所以經過設置這個選項爲true,能夠有以下效果:當2個小時(具體的實現而不一樣)內在任意方向上都沒有跨越套接字交換數據,則 TCP 會自動發送一個保持存活的消息到對面。將會有如下三種響應:

  1. 返回指望的ACK。那麼不通知應用程序(由於一切正常),2 小時的不活動時間事後,TCP 將發送另外一個探頭。
  2. 對面返回RST,代表對面掛了,可是又好了,Socket依然要關閉
  3. 沒有響應,說明對面掛了,這時候關閉Socket

  因此對於構建長時間鏈接的Socket仍是配置上SO_KEEPALIVE比較好。

 

異常:java.net.SocketException: Connection reset by peer

 

  這個異常的含義是,我正在寫數據的時候,你把鏈接給關閉了。這個異常在通常正常的編碼是不會出現這個異常的,由於用戶一般會判斷是否讀到流的末尾了,讀到末尾纔會進行關閉操做,若是出現這個異常,那就檢查一下判斷是否讀到流的末尾邏輯是否正確。

 

拆包和黏包

  使用Socket通訊的時候,或多或少都聽過拆包和黏包,若是沒聽過而去貿然編程那麼偶爾就會碰到一些莫名其妙的問題,全部有這方面的知識仍是比較重要的,至少知道怎麼發生,怎麼防範。

  如今先簡單說明下拆包和黏包的緣由:

  • 拆包:當一次發送(Socket)的數據量過大,而底層(TCP/IP)不支持一次發送那麼大的數據量,則會發生拆包現象。
  • 黏包:當在短期內發送(Socket)不少數據量小的包時,底層(TCP/IP)會根據必定的算法(指Nagle)把一些包合做爲一個包發送。

  首先能夠明確的是,大部分狀況下咱們是不但願發生拆包和黏包的(若是但願發生,什麼都去作便可),那麼怎麼去避免呢,下面進行詳解?

 

黏包

  首先咱們應該正確看待黏包,黏包其實是對網絡通訊的一種優化,假如說上層只發送一個字節數據,而底層卻發送了41個字節,其中20字節的I P首部、 20字節的T C P首部和1個字節的數據,並且發送完後還須要確認,這麼作浪費了帶寬,量大時還會形成網絡擁堵。固然它仍是有必定的缺點的,就是由於它會合並一些包會致使數據不能當即發送出去,會形成延遲,若是能接受(通常延遲爲200ms),那麼仍是不建議關閉這種優化,若是由於黏包會形成業務上的錯誤,那麼請改正你的服務端讀取算法(協議),由於即使不發生黏包,在服務端緩存區也可能會合並起來一塊兒提交給上層,推薦使用長度+類型+數據模式。

若是不但願發生黏包,那麼經過禁用TCP_NODELAY便可,Socket中也有相應的方法:

void setTcpNoDelay(boolean on) 

  經過設置爲true便可防止在發送的時候黏包,可是當發送的速率大於讀取的速率時,在服務端也會發生黏包,即因服務端讀取過慢,致使它一次可能讀取多個包。

 

拆包

  這個問題應該引發重視,在TCP/IP詳解中說過:最大報文段長度(MSS)表示TCP傳往另外一端的最大塊數據的長度。當一個鏈接創建時,鏈接的雙方都要通告各自的 MSS。客戶端會盡可能知足服務端的要求且不能大於服務端的MSS值,當沒有協商時,會使用值536字節。雖然看起來MSS值越大越好,可是考慮到一些其餘狀況,這個值仍是不太好肯定,具體詳見《TCP/IP詳解 卷1:協議》。

  如何應對拆包,其實在上面內容已經介紹過了,那就是如何代表發送完一條消息了,對於已知數據長度的模式,能夠構造相同大小的數組,循環讀取,示例代碼以下:

int length=1024;//這個是讀取的到數據長度,現假定1024 byte[] data=new byte[1024]; int readLength=0; while(readLength<length){ int read = inputStream.read(data, readLength, length-readLength); readLength+=read; }

這樣當循環結束後,就能讀取到完整的一條數據,而不須要考慮拆包了。





基於UDP協議的Socket的使用

服務端
 
package com.example.admin.server;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;


public class MyClass {

public static void main(String[] args) throws IOException {
DatagramSocket socket = null;
try {
socket = new DatagramSocket(8888);
System.out.println("服務器開始監聽消息");
} catch (Exception e) {
e.printStackTrace();
}
while (true) {
byte data[] = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
socket.receive(packet);
String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("客戶端說: " + result);
HandleThread handleThread = new HandleThread(socket,packet);
handleThread.setPriority( 3 );
handleThread.start();
}
}

static class HandleThread extends Thread {

private DatagramSocket mSocket;
private DatagramPacket packet;

public HandleThread(DatagramSocket mSocket ,DatagramPacket packet) {
super();
this.mSocket=mSocket;
this.packet=packet;

}

@Override
public void run() {
try {
byte[] sendData = "服務端說:Welcome to the new world!".getBytes("UTF-8");
SocketAddress remoteAddress = packet.getSocketAddress();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, remoteAddress);
mSocket.send(sendPacket);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
 
android客戶端
 
package com.example.admin.socket;


import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class MainActivity extends AppCompatActivity {

private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView=findViewById(R.id.textview);
Button button=findViewById(R.id.button);

button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
super.run();
try {
call();
}catch (IOException e){
Log.e("io","io close failed");
}

}
}.start();}

});
}


public void call()throws IOException {
DatagramSocket mSocket=null;
try {
// 1.初始化DatagramSocket
mSocket = new DatagramSocket();
mSocket = new DatagramSocket();
InetAddress address = InetAddress.getByName("xxx.xxx.x.x");//就是前面獲取的服務器IP地址
int port=8888;
String sendData = "hello world";
byte data[] = sendData.getBytes("UTF-8");
DatagramPacket packet = new DatagramPacket(data, data.length, address, 8888);
mSocket.send(packet);
final byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
mSocket.receive(receivePacket);
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(new String(receiveData));
}
});
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
} finally {
if (mSocket != null) {
mSocket.close();
}
}
}

}
 
控制檯輸出:服務器開始監聽消息 客戶端說: hello world 客戶端說: hello world文章大量內容來自:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html
相關文章
相關標籤/搜索