使用NIO進行阻塞式通信-基於TCP協議

使用NIO進行阻塞式通信-基於TCP協議

與使用SocketAPI進行網絡通信的方法很類似。
在使用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創建連接時,會進行“三次握手”:

  1. 當客戶端的SYN請求到達服務器時,把請求放入syn隊列;
  2. 服務端向客戶端發送SYN+ACK報文,等待客戶端的ACK報文;
  3. 當客戶端的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("連接服務器失敗");
		}
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章