什么是网络编程
网络编程是指编写运行在多个设备上(计算机)的程序, 通过网络进行数据交换. 比如现在流行的微服务, 把一个大的系统按照功能拆分多个微服务, 每个微服务都是一个独立的应用, 部署在不同的服务器上, 不同服务器上的微服务如何进行通信就是属于网络编程的范畴.
TCP/UDP、IP、HTTP、Socket的区别
网络模型(OSI)从下往上分为七层, 分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层. IP协议是属于网络层的协议, TCP/UDP是属于传输层的协议, HTTP是属于应用层的协议, Socket则是对TCP/IP协议和UDP/IP协议的封装和应用.
网络编程三要素
网络编程三要素分别是IP、端口号、TCP/UDP协议. IP是每个设备在网络中的唯一标识, 端口号是每个程序在设备上的唯一标识, TCP/UDP是数据传输的协议.
(1)TCP协议和UDP协议的区别
- TCP协议是面向连接的(三次握手), 数据安全, 速度慢.
- UDP协议是面向无连接的, 数据不安全, 速度快.
(2)TCP协议的三次握手
- 第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers).
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次挥手”.
(3)TCP协议的四次挥手
- 第一次挥手:当主机A的应用程序通知TCP数据已经发送完毕时,TCP向主机B发送一个带有FIN附加标记的报文段(FIN表示英文finish)。
- 第二次挥手:主机B收到这个FIN报文段之后,并不立即用FIN报文段回复主机A,而是先向主机A发送一个确认序号ACK,同时通知自己相应的应用程序:对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段。
- 第三次挥手:主机B的应用程序告诉TCP:我要彻底的关闭连接,TCP向主机A送一个FIN报文段。
- 第四次挥手:主机A收到这个FIN报文段后,向主机B发送一个ACK表示连接彻底释放。
BIO(Blocking IO)
BIO也叫同步阻塞IO, 对于每一个客户端的连接请求都会创建一个新线程来进行处理, 处理完成后线程销毁. 当一个线程调用IO流读写数据时,该线程被阻塞,直到读到数据,或数据完全写入, 该线程在此期间不能再干任何事情。
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(12000);
System.out.println("server start...");
while(true){
//进行阻塞,监听端口
Socket socket = server.accept();
//新建一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
public class ServerHandler implements Runnable{
private Socket socket ;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String content = null;
while(true){
content = in.readLine();
if(content == null) break;
System.out.println("Server :" + content);
out.println("Server response");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
public class Client {
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("127.0.0.1", 12000);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服务器端发送数据
out.println("Client request");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
伪异步IO
使用线程池来管理线程, 实现1个或多个线程处理N个客户端.
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(12000);
System.out.println("server start...");
while(true){
//进行阻塞,监听端口
Socket socket = server.accept();
//用线程池来管理线程
ThreadPoolExecutor executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
10,
120L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(20)
);
//新建一个线程执行客户端的任务
executor.execute(new Thread(new ServerHandler(socket)));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
NIO(Non-Blocking IO)
NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件.
Buffer
在BIO中, 数据直接读写到Stream对象中. 而在NIO中, 所有数据都是读写到Buffer中, 然后通过Channel传输. Buffer实质上是一个数组, 通常它是一个字节数组(ByteBuffer) ,也可以是其他类型的数组.
(1)成员变量
- mark : s初始值为-1,用于备份当前的position;
- position : 初始值为0, position表示当前可以写入或读取数据的位置,当写入或读取一个数据后,position移动到下一个位置
- limit : 写模式下,limit表示最多能往Buffer里写多少数据,等于capacity值;读模式下,limit表示最多可以读取多少数据
- capacity : 缓存数组大小
(2)成员方法
- allocate(int capacity) : 创建指定长度的缓冲区
- put(E e) : 添加一个元素
- get() : 获取第一个元素
- wrap(E[] array) : 将数组元素添加到缓冲中
clear() : 清除数据, 实际上数据没有被清除.
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
flip() : Buffer有两种模式, 写模式和读模式. flip后Buffer从写模式变成读模式.
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
(3)Buffer的实现类
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
(4)实例
public static void main(String[] args) {
//创建指定长度的缓冲区
IntBuffer buf = IntBuffer.allocate(10);
buf.put(13);
buf.put(21);
buf.put(35);
System.out.println(buf);
//从写模式变成读模式
buf.flip();
System.out.println(buf);
//调用get方法会使position位置向后递增一位
for (int i = 0; i < buf.limit(); i++) {
System.out.print(buf.get() + "\t");
}
System.out.println("\n" + buf);
//清除数据
buf.clear();
System.out.println(buf);
}
java.nio.HeapIntBuffer[pos=3 lim=10 cap=10]
java.nio.HeapIntBuffer[pos=0 lim=3 cap=10]
13 21 35
java.nio.HeapIntBuffer[pos=3 lim=3 cap=10]
java.nio.HeapIntBuffer[pos=0 lim=10 cap=10]
Channel
NIO把它支持的I/O对象抽象为Channel, Channel又称为”通道”, 类似于BIO中的流(Stream), 但有锁区别:
- 流是单向的,通道是双向的,可读可写
- 流读写是阻塞的,通道可以阻塞也可以非阻塞
- 流中的数据可以选择性的先读到缓存中,通道的数据总是要先读写到缓存中
(1)Channel的实现类
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
Selector
Selector选择器充当一个监听者, 会不断地轮询注册在其上的通道(Channel), 如果某个通道发生了读写操作, 这个通道就处于就绪状态, 会被Selector轮询出来, 进行后续的IO操作.
(1)Selector监听的事件(SelectionKey)
- OP_CONNECT : 客户端连接服务端事件
- OP_ACCEPT : 服务端接收客户端连接事件
- OP_READ : 读事件
- OP_WRITE : 写事件
简单的NIO实例
public class Server{
//选择器(多路复用器)
private Selector selector;
//读取的缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//写入的缓冲区
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
/**
* 打开选择器,注册服务器通道
*/
public Server(int port){
try {
//1 打开选择器
this.selector = Selector.open();
//2 打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
// 5 把服务器通道注册到选择器上,并为该通道注册OP_ACCEPT事件.
// 当该事件到达时,selector.select()会返回,否则selector.select()会一直阻塞
ssc.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*/
public void listen() {
while(true){
try {
//1 当注册的事件发生时,方法返回;否则,该方法会一直阻塞
this.selector.select();
//2 获得selector选中项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
//删除已选的key,以防重复处理
keys.remove();
//OP_ACCEPT事件发生(接收到客户端的连接)
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//获得和客户端连接的通道
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//将SocketChannel注册到selector上,并设置读事件
sc.register(this.selector, SelectionKey.OP_READ);
}else if(key.isReadable()){ //OP_READ事件发生
read(key);
}else if(key.isWritable()){ //OP_WRITE事件发生
write(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读操作
*/
private void read(SelectionKey key) {
try {
//清空缓冲区旧的数据
this.readBuf.clear();
//获取socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
int count = sc.read(this.readBuf);
if(count > 0){
String msg = new String(readBuf.array());
System.out.println("服务端收到信息:"+msg);
//将SocketChannel注册到selector上,并设置写事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 写操作
*/
private void write(SelectionKey key){
try {
//清空缓冲区旧的数据
this.writeBuf.clear();
//获取socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap(new String("我是服务端,我已收到你的信息!").getBytes()));
//将SocketChannel注册到selector上,并设置读事件
sc.register(this.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Server(12000).listen();
}
}
public class Client {
//选择器(多路复用器)
private Selector selector;
//读取的缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//写入的缓冲区
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
/**
* 打开选择器,注册客户端通道
*/
public Client(String ip,int port){
try {
//1 打开选择器
this.selector = Selector.open();
//2 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
//3 设置通道为非阻塞
channel.configureBlocking(false);
//4 绑定服务器ip和port
channel.connect(new InetSocketAddress(ip,port));
//5 将客户端通道注册到选择器上,并为该通道注册OP_CONNECT事件
channel.register(selector, SelectionKey.OP_CONNECT);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*/
public void listen() {
while(true){
try {
//1 当注册的事件发生时,方法返回;否则,该方法会一直阻塞
this.selector.select();
//2 获得selector选中项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
while(keys.hasNext()){
SelectionKey key = keys.next();
//删除已选的key,以防重复处理
keys.remove();
//OP_CONNECT事件发生(连接上服务器)
if(key.isConnectable()){
SocketChannel sc = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if(sc.isConnectionPending()){
sc.finishConnect();
}
sc.configureBlocking(false);
//将SocketChannel注册到selector上,并设置写事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}else if(key.isReadable()){
read(key);
}else if(key.isWritable()){
write(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读操作
*/
private void read(SelectionKey key) {
try {
//清空缓冲区旧的数据
this.readBuf.clear();
//获取socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
int count = sc.read(this.readBuf);
if(count > 0){
String msg = new String(readBuf.array());
System.out.println("客户端收到信息:"+msg);
//将SocketChannel注册到selector上,并设置写事件
sc.register(this.selector, SelectionKey.OP_WRITE);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 写操作
*/
private void write(SelectionKey key){
try {
//清空缓冲区旧的数据
this.writeBuf.clear();
//获取socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
sc.write(ByteBuffer.wrap(new String("我是Client,我先发条信息!").getBytes()));
//将SocketChannel注册到selector上,并设置读事件
sc.register(this.selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Client("127.0.0.1",12000).listen();
}
}
结果:
客户端先发送一条消息
服务端收到客户端的消息,然后返回一条消息
客户端收到服务端的消息,再发送一条消息
...
BIO和NIO的区别
(1)BIO是面向流的, 而NIO是面向缓冲的.
(2)BIO是阻塞的,而NIO是非阻塞的.
- 传统IO方式(BIO)在调用InputStream.read()/BufferedReader.readLine()方法时是阻塞的,它会一直等到数据到来或缓冲区已满或超时才会返回.
- NIO通过向Selector注册读写事件, Selector不断轮询读写事件是否发生, 当读写事件发生后再去进行相应的处理.
(3)NIO的选择器允许一个单独的线程来监视多个输入通道.