使用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("连接服务器失败");
		}
	}
}

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