1、找到配置文件

1
2
3
4
5
6
[root@localhost network-scripts]# cd /etc/sysconfig/network-scripts/
[root@localhost network-scripts]# ls
ifcfg-ens33 ifdown-bnep ifdown-ipv6 ifdown-ppp ifdown-Team ifup ifup-eth ifup-isdn ifup-post ifup-sit ifup-tunnel network-functions
ifcfg-lo ifdown-eth ifdown-isdn ifdown-routes ifdown-TeamPort ifup-aliases ifup-ippp ifup-plip ifup-ppp ifup-Team ifup-wireless network-functions-ipv6
ifdown ifdown-ippp ifdown-post ifdown-sit ifdown-tunnel ifup-bnep ifup-ipv6 ifup-plusb ifup-routes ifup-TeamPort init.ipv6-global
[root@localhost network-scripts]#

Read More

1、协议实现方式说明

  1. 序列化
    • 使用netty3中的ChannelBuffers类提供的dynamicBuffer()方法创建动态长度可变的ChannelBuffer,用于存储序列化后的字节数组
    • 对于基本类型(byte、short、int、long、double、float、double),使用ChannelBuffer的writeXxx方法写入
    • 对于String类型数据,采用short+Byte[]的格式写入,其中short表示String转成byte数组后数组的长度
    • 对于List类型数据,采用short+Object+Object...格式写入,其中short表示list中元素的个数,Object为List中的元素
    • 对于Map类型数据,采用short+KeyObject+ValueObject+KeyObject+ValueObject...格式写入,其中short表示map中元素的个数
    • 对于其他类型数据,需继承一个基类,继承该基类的类需提供写入方法的具体实现
  2. 反序列化
    • 使用netty3中的ChannelBuffers类提供的copiedBuffer(byte[] array)方法创建ChannelBuffer对象,通过该ChannelBuffer对象来读取属性值
    • 对于基本类型(byte、short、int、long、double、float、double),使用ChannelBuffer的readXxx方法读取
    • 对于String类型数据,先读取short即字符串字节数组长度size,再通过size读取字符串
    • 对于List类型的数据,先读取short即List中元素个数,再循环读取信息加入到List中
    • 对于Map类型的数据,先读取short即Map中元素个数,再循环读取key值和value值加入到Map中
    • 对于其他数据类型,需继承一个基类,继承该基类的类需提供读取方法的具体实现

Read More

1、下载并配置环境变量

​ 下载地址如下:https://github.com/google/protobuf

2、编写proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";
option java_package = "io.github.lin38.serialize.demo006.model";
option java_outer_classname = "PlayerModule";

message PBPlayer{
int64 playerId = 1;

int32 age = 2;

string name = 3;

repeated int32 skills = 4;
}

message PBResource{
int64 gold = 1;

int32 energy = 2;
}

Read More

1、序列化与反序列化

  把对象转换为字节序列的过程称为对象的序列化。
  把字节序列恢复为对象的过程称为对象的反序列化。
  对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列。

  在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些session先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

2、JDK关于序列化的API

  java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

​ java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

​ 只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。

​ 对象序列化包括如下步骤:

​ 1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;

​ 2) 通过对象输出流的writeObject()方法写对象。

​ 对象反序列化的步骤如下:

​ 1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;

​ 2) 通过对象输入流的readObject()方法读取对象。

Read More

1、为什么使用Selector

​ Selector(选择器)能够注册一或多个通道,并能够监听通道的读写事件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

​ 仅用单个线程来处理多个Channel的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,不要频繁的创建线程。

​ 下面是单线程使用一个Selector处理3个channel的示例图:

2、创建Selector

1
Selector selector = Selector.open();

Read More

1、通道与流的区别

​ Java NIO的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

2、Channel的实现

  • FileChannel:从文件中读写数据
  • DatagramChannel:能通过UDP读写网络中的数据
  • SocketChannel:能通过TCP读写网络中的数据
  • ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel

Read More

1、Buffer的基本用法

​ 使用Buffer读写数据一般遵循以下四个步骤:

  1. 写入数据到Buffer

  2. 调用flip()方法

  3. 从Buffer中读取数据

  4. 调用clear()方法或者compact()方法

    当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

    一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.part_02_nio.demo004;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

public class UseFileChannel {
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

// create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

List<byte[]> byteList = new ArrayList<>(); // 从channel中每次read到的byte[]
int allLength = 0; // 所有循环之后,read到的总byte[]长度

int bytesRead = inChannel.read(buf); // read into buffer.
while (bytesRead != -1) {

buf.flip(); // make buffer ready for read

byte[] bytes = new byte[bytesRead]; // 用于存储本次循环read到的byte[]
allLength += bytesRead;

int index = 0;
while (buf.hasRemaining()) {
bytes[index] = buf.get();
index++;
// System.out.print((char) buf.get()); // read 1 byte at a time
}
byteList.add(bytes);

buf.clear(); // make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();

byte[] all = new byte[allLength]; // 把每次循环read到的byte[]归并到一个总的数组中
int current = 0;
for (byte[] bs : byteList) {
System.arraycopy(bs, 0, all, current, bs.length);
current += bs.length;
}

System.out.println(new String(all));
}
}

Read More

1、单Selector问题

​ 对于并发不是很大的情况下,一个Selector负责轮询注册事件的变化,并对事件进行处理,是没有问题的。但是当并发比较大的时候,客户端连接比较多,且IO也比较多的情况,一个Selector肯定是不能满足对于系统性能的要求。

​ 对于高并发的情况,给去的解决方案是:分为boss(一个或多个)和worker(多个)两种线程来处理,每一个线程都拥有自己的Selector。boss线程负责接收客户端请求,将accept到的SocketChannel转交给其中的一个worker线程去处理IO请求。

​ 结合上一篇文章的图解,这一篇实现的效果,类似于:

​ 通俗解释:用餐高峰期,餐厅客人较多,一个服务员肯定是忙不过来的,这个时候就需要多个服务员来接待客人了。

Read More

1、NIO的方式实现服务端、客户端通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.example.part_02_nio.demo001;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
* NIO服务端
*/
public class Server {
// 通道管理器
private Selector selector;

/**
* 获得一个ServerSocketChannel,并对该通道做一些初始化的工作
*
* @param port
* 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}

/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 进行处理
handler(key);
}
}
}

/**
* 处理请求
*
* @param key
* @throws IOException
*/
public void handler(SelectionKey key) throws IOException {
if (key.isAcceptable()) { // 客户端请求连接事件
handlerAccept(key);
} else if (key.isReadable()) { // 获得了可读的事件
handelerRead(key);
}
}

/**
* 处理连接请求
*
* @param key
* @throws IOException
*/
public void handlerAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);

// 在这里可以给客户端发送信息哦
// 回写数据
ByteBuffer outBuffer = ByteBuffer.wrap("成功建立连接".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
System.out.println("新的客户端连接");
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
// 回写数据时候,可以监听写事件
channel.register(this.selector, SelectionKey.OP_READ);
}

/**
* 处理读的事件
*
* @param key
* @throws IOException
*/
public void handelerRead(SelectionKey key) throws IOException {
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if(read > 0){
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:" + msg);

// 回写数据
ByteBuffer outBuffer = ByteBuffer.wrap(("好的" + msg).getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}else{
System.out.println("客户端关闭");
key.cancel();
}
}

/**
* 启动服务端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
Server server = new Server();
server.initServer(8899);
server.listen();
}

}

Read More

1、传统的BIO编程

​ 网络编程的基本模型是Server/Client模型,也就是两个进程直接进行相互通信,其中服务端提供配置信息(绑定的IP地址和监听的端口),客户端通过连接操作向服务端监听的地址发送连接请求,通过三次握手建立连接,如果连接成功,则双方即可进行通信(网络套接字Socket)。

2、传统的BIO实现通讯的方式及优缺点

2.1 服务端单线程形式

  1. BIO的B即blocking,阻塞的意思,那么阻塞的点在哪呢?

    • server.accept();:服务端程序会一直阻塞在此处,直到有一个客户端连接过来(客户端程序存在此阻塞点)
    • reader.readLine();:服务端程序会一直阻塞在此处,直到客户端发送消息过来
  2. 同一时刻,只能处理一个客户端请求

Read More