Socket概述
Socket套接字就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何 在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协 议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信 息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
Socket整体流程
Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字 (ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到 65536,但是0到1024是为特权服务保留的端口号,可以选择任意一个当前没有被其他进程使用的端 口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
Io操作概述
Io操作就是指对系统中数据(流的形式)的接收发送操作。
主要分为两个阶段
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
Io模型概述
IO模型就是说用什么样的通道进行数据的发送和接收,Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)
- 同步:指的是用户发起io操作之后,(阻塞模式:阻塞等待数据准备完成,非阻塞模式:通过select轮询遍历(通知)查看数据是否准备完成),当数据到达后,用户线程会阻塞,直到IO操作完成。
- 异步:指的是用户线程发起io操作之后,知道被通知数据处理完成都是没有阻塞的。
再举个例子:
xialu要来xiami家里来玩,所以xiami计划到机场去接xialu.
- 同步阻塞
xiami一大早就开车到机场等着,等了老半天接到了xialu,然后开车带到家里。
- 同步非阻塞
xialu下飞机之后给xiami发短信,xiami时不时看下手机有没有收到短信,收到xialu的短信之后到机场接xialu,然后开车带回家。(开车带回家这段时间没办法省略,对于xiami来说相当于阻塞,因为没法干其他事)。
- 异步非阻塞
xialu下飞机之后做xiayi的车到xiami家,到了之后给xiami电话(通知)。对于xiami来说,xialu到达机场,再从机场到xiami家,都是异步的,整个过程都可以干其它事情。
BIO(同步并阻塞)
同步阻塞模型,一个客户端连接对应一个处理线程
缺点分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
- 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
- IO代码里read操作是阻塞操作,如果建立连接后当前线程暂时没有数据可读,则线程会阻塞在Read 操作上,造成线程资源浪费
应用场景分析
BIO方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。
Bio代码示例
服务器端
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(9999);
while (true) {
System.out.println("等待客户端连接...");
Socket accept = socket.accept();
System.out.println("客户端连接成功...");
handler(accept);
}
}
public static void handler(Socket socket) throws IOException {
System.out.println("线程id:" + Thread.currentThread().getId());
byte[] bytes = new byte[1024];
System.out.println("服务器准备从客户端读取数据...");
int read = socket.getInputStream().read(bytes);
if (read != -1) {
System.out.println("读取到的客户端数据:" + new String(bytes, 0, read));
}
System.out.println("服务器从客户端读取数据结束...");
System.out.println("服务器准备往客户端写数据...");
socket.getOutputStream().write("Hellow World".getBytes());
socket.getOutputStream().flush();
System.out.println("服务器往客户端写数据结束...");
}
}
客户端
public class BioClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9999);
System.out.println("客户端往服务器写数据...");
socket.getOutputStream().write("Hellow Server".getBytes());
System.out.println("客户端往服务器写数据结束...");
byte[] bytes = new byte[1024];
System.out.println("客户端准备从服务器读数据...");
int read = socket.getInputStream().read(bytes);
if (read != -1) {
System.out.println(new String(bytes, 0, read));
}
System.out.println("客户端从服务器读取数据结束...");
}
}
NIO(同步非阻塞)
同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用 器轮询到连接有IO请求就进行处理。
I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现,他们的区别如下
应用场景分析
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂, JDK1.4 开 始支持。
Nio代码示例
服务器端
public class NioServer {
//public static ExecutorService pool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
//必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9000));
// 创建一个选择器selector
Selector selector = Selector.open();
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("等待事件发生。。");
// 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
int select = selector.select();
System.out.println("有事件发生了。。");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//删除本次已处理的key,防止下次select重复处理
it.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
System.out.println("有客户端连接事件发生了。。");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
//处理完连接请求不会继续等待客户端的数据发送
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//通过Selector监听Channel时对读事件感兴趣
sc.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("有客户端数据可读事件发生了。。");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件");
// NIO事件触发是水平触发
// 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
// 在有数据往外写的时候再注册写事件
key.interestOps(SelectionKey.OP_READ);
//sc.close();
}
}
}
客户端
public class NioClient {
//通道管理器
private Selector selector;
/**
* 启动客户端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NioClient client = new NioClient();
client.initClient("127.0.0.1", 9000);
client.connect();
}
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
*
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip, int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect() 才能完成连接
channel.connect(new InetSocketAddress(ip, port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void connect() throws IOException {
// 轮询访问selector
while (true) {
selector.select();
// 获得selector中选中的项的迭代器
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
// 删除已选的key,以防重复处理
it.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
channel.write(buffer);
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
AIO(异步非阻塞)
AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,JDK7 开始支持.
应用场景分析
AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分 调用 OS 参与并发操作, 编程比较复杂。
Aio代码示例
服务端实现
public class AioServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
serverChannel.accept(attachment, this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("hello xiami".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
}
客户端
public class AioClient {
public static void main(String... args) throws Exception {
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
socketChannel.write(ByteBuffer.wrap("Hello xialu".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(512);
Integer len = socketChannel.read(buffer).get();
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len)); }
}
}