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