使用NIO進行阻塞式通信-基於TCP協議
與使用Socket
API進行網絡通信的方法很類似。
在使用Socket
進行通信時服務端使用ServerSocket
監聽連接;使用NIO,則使用ServerSocketChannel
監聽連接。
對於非阻塞式的通信,通信的流程不變:
文章目錄
服務端
1. 創建ServerSocketChannel
java.nio.channels.ServerSocketChannel
提供了一個工廠方法open()
,通過該工廠方法,創建一個ServerSocketChannel的實例。
舉例:
ServerSocketChannel severSocketChannel = ServerSocketChannel.open();
2. 綁定本地端口,開始監聽
ServerSocketChannel
提供了實例方法bind(SocketAddress local)
SocketAddress
是一個抽象類,表示地址。對於IPv4
協議,使用InetSocketAddress
舉例:
severSocketChannel.bind(new InetSocketAddress(PORT));
其實,bind(ScoketAddress)
的實現是調用了bind(SocketAddress, 0)
。第二個參數是int backlog
,表示Accept隊列的大小。
簡單說一下accept隊列,TCP創建連接時,會進行“三次握手”:
- 當客戶端的
SYN
請求到達服務器時,把請求放入syn隊列; - 服務端向客戶端發送
SYN+ACK
報文,等待客戶端的ACK
報文; - 當客戶端的
ACK
報文到達服務器時,會把syn隊列中的請求放入accept隊列
調用accept()其實就是從accept隊列取連接
bind()
除了會綁定地址,還會開始監聽客戶端連接
3. 調用accept(),連接到客戶端
通過調用ServerSocketChannel.accept()
,可以得到請求連接的客戶端的SocketChannel
舉例:
SocketChannel clientChannel = this.severSocketChannel.accept();
4. 通過SocketChannel
進行通信
通過accept()
得到SocketChannel
,這個socket通道是一個雙向通道,提供了read(ByteBuffer)
和write(ByteBuffer)
進行通信。
舉例:
這個例子是一個echo
服務端的簡單實現
while(this.clientChannel.read(buffer) != -1) {
buffer.flip();
//服務端輸出客戶端發來的消息
String content = new String(buffer.array(), 0, buffer.limit());
if("exit".equals(content)) {
break;
}
System.out.println("收到信息 [" + clientIp + " : " + clientPort + "] " + content);
//回送給客戶端
this.clientChannel.write(buffer);
//準備下一次輸出
buffer.clear();
}
通常,在讀取信息時,不會使用-1
作爲定界符,一般使用自定義的定界符,只有當客戶端關閉通道的輸出socketChannel.shutdownOutput()
時,纔會發送-1
(當關閉socketChannle時,也會關閉Output)。在通信時,服務端可能有多次的讀取(多個循環),如果使用-1做定界符,只有客戶端關閉Output,纔會跳出第一個循環
5. 通信完成,關閉與客戶端的連接SocketChannel.close()
舉例:
示例代碼中的clientChannel,是通過accepte()接收的SocketChannel的實例
clientChannel.close();
6. 關閉服務端ServerSocketChannel.close()
示例:Echo服務端,支持多客戶端連接
爲了支持多個客戶端連接,每一個連接都需要一個線程進行處理(EchoHandler
就是爲了處理每一個連接);
public class Server {
private static final int PORT = 8888;
private ServerSocketChannel severSocketChannel;
public Server(){
try {
this.severSocketChannel = ServerSocketChannel.open();
this.severSocketChannel.bind(new InetSocketAddress(PORT));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static class EchoHandler extends Thread{
private SocketChannel clientChannel;
private String ip;
private int port;
public EchoHandler(SocketChannel clientChannel) {
this.clientChannel = clientChannel;
try {
InetSocketAddress address = (InetSocketAddress)clientChannel.getLocalAddress();
ip = address.getHostString();
port = address.getPort();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
try {
while(this.clientChannel.read(buffer) != -1) {
buffer.flip();
//服務端輸出
String content = new String(buffer.array(), 0, buffer.limit());
if("exit".equals(content)) {
break;
}
System.out.println("收到信息 [" + this.ip + " : " + this.port + "] " + content);
//回送給客戶端
this.clientChannel.write(buffer);
//準備下一次輸出
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("與 [ " + this.ip + " : " + this.port + "]的連接已斷開");
try {
this.clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void Service() {
while(true) {
try {
SocketChannel clientChannel = this.severSocketChannel.accept();
(new EchoHandler(clientChannel)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
(new Server()).Service();
}
}
客戶端
1. 創建SocketChannel
使用靜態工廠方法java.nio.channels.SocketChannel.open()
,可以創建一個SocketChannel的實例。
2. 綁定地址bind()
通過SocketChannel.bind(SocketAddress)
綁定地址。
3. 連接服務端connect()
通過SocketChannel.connect(SocketAddress remote)
連接到指定的服務端。
注意:創建SocketChannel還有另一個工廠方法open(SocketAddress remote)
,使用這個方法創建時,還會調用connect()
方法連接到指定的服務端;這時,沒有顯式的調用bind()綁定地址,會自動生成一個
4. 使用SocketChannel
通信
與服務端使用SocketChannel
通信方法相同。
5. 關閉連接SocketChannel.close()
示例:連接到Echo服務端的客戶端
public class Client {
private static final String serverIp = "127.0.0.1";
private static final int serverPort = 8888;
private static int localPort = 9999;
private static final Charset charset = Charset.forName("UTF-8");
private SocketChannel socketChannel;
private ByteBuffer inputBuffer;
private ByteBuffer outputBuffer;
public Client() {
this(localPort);
}
public Client(int localPort) {
try {
this.socketChannel = SocketChannel.open();
this.socketChannel.bind(new InetSocketAddress(localPort));
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean connect() throws IOException {
boolean isSuccess = this.socketChannel.connect(new InetSocketAddress(serverIp, serverPort));
return isSuccess;
}
public void send(String content) throws IOException {
if(outputBuffer == null) {
outputBuffer = ByteBuffer.allocate(1024);
}
//編碼
outputBuffer.put(charset.encode(content));
//發送給服務端
outputBuffer.flip();
socketChannel.write(outputBuffer);
outputBuffer.clear();
}
public String recv() throws IOException {
if(inputBuffer == null) {
inputBuffer = ByteBuffer.allocate(1024);
}
socketChannel.read(inputBuffer);
inputBuffer.flip();
String rtv = new String(inputBuffer.array(), 0, inputBuffer.limit());
inputBuffer.clear();
return rtv;
}
public static void main(String[] args) throws IOException {
Client client = new Client();
boolean isSuccess = client.connect();
if(isSuccess) {
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
while(true) {
//從標準輸入讀入
try {
String inputContent = input.readLine();
//發送給服務端
client.send(inputContent);
if("exit".equals(inputContent)) {
break;
}
//從服務端接收數據
String receiveContent = client.recv();
System.out.println("echo : " + receiveContent);
} catch (IOException e) {
e.printStackTrace();
}
}
}else {
System.err.println("連接服務器失敗");
}
}
}