MOOC 个人学习笔记
# 1.NIO 编程
- Non-Blocking I/O
- 提供非阻塞通讯等方式
- 避免同步 I/O 通讯效率过低
- 一个线程可以管理多个连接
- 减少线程多的压力
# 主要类
- Buffer 缓存区
- Channel 通道
- Selector 多路选择器
# Buffer
- Buffer 缓冲区,一个可以读写的内存区域
- ByteBuffer, CharBuffer, DoubleBuffer, IntBuffer, LongBuffer, ShortBuffer (StringBuffer 不是 Buffer 缓冲区)
- 四个主要属性
- capacity 容量
- position 读写位置
- limit 界限
- mark 标记,用于重复一个读 / 写操作
- 使用 Buffer 读写数据一般遵循以下四个步骤:
- 写入数据到 Buffer
- 调用 flip () 方法
- 从 Buffer 中读取数据
- 调用 clear () 方法或者 compact () 方法
- Buffer 的分配
- allocate 方法
ByteBuffer buf = ByteBuffer.allocate(48);
- 向 Buffer 中写数据
- 从 Channel 写到 Buffer
int bytesRead = inChannel.read(buf);
- 通过 Buffer 的 put () 方法写入 Buffer
buf.put(127);
- flip () 方法
- flip 方法将 Buffer 从写模式切换到读模式
- 调用 flip () 方法会将 position 设回 0,并将 limit 设置成之前 position 的值
- 从 Buffer 中读数据
- 从 Buffer 读取数据到 Channel
int bytesWritten = inChannel.write(buf);
- 使用 get () 方法从 Buffer 中读取数据
byte aByte = buf.get();
- clear () 与 compact () 方法
- clear()
- position 将被设回 0,limit 被设置成 capacity 的值
- Buffer 被清空了,但 Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据
- compact()
- 将所有未读的数据拷贝到 Buffer 起始处,然后将 position 设到最后一个未读元素正后面
- limit 属性依然像 clear () 方法一样,设置成 capacity
- 不会覆盖未读的数据
- clear()
# Channel
- 全双工的,支持读 / 写 (而 Stream 流是单向的)
- 支持异步读写
- 和 Buffer 配合,提高效率
- ServerSocketChannel 服务器 TCP Socket 接入通道,接收客户端
- SocketChannel TCP Socket 通道,可支持阻塞 / 非阻塞通讯
- DatagramChannel UDP 通道
- FileChannel 文件通道
# SocketChannel
- 打开 SocketChannel
SocketChannel socketChannel = SocketChannel.open(); | |
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80)); |
- 关闭 SocketChannel
- close()
socketChannel.close();
- ** 读取 SocketChannel **
- read()
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
- 写入 SocketChannel
- write()
String newData = "New String to write to file...";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
# Selector
- 每隔一段时间,不断轮询注册在其上的 Channel
- 如果有一个 Channel 有接入、读、写操作,就会被轮询出来
- 根据 SelectionKey 可以获取相应的 Channel,进行后续 IO 操作
- 避免过多的线程
- SelectionKey 四种类型
- OP_CONNECT (连接就绪 某个 Channel 成功连接到另一个服务器)
- OP_ACCEPT (接收就绪 一个 ServerSocketChannel 准备好接收新进入的连接)
- OP_READ (读就绪 有数据可读的通道)
- OP_WRITE (写就绪 等待写数据的通道)
- Selector 的创建
- Selector.open () 方法
Selector selector = Selector.open();
- 向 Selector 注册通道
- SelectableChannel.register () 方法
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
- 通过 Selector 选择通道
- 一旦向 Selector 注册了一或多个通道,就可以调用几个重载的 select () 方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道
- select () 阻塞到至少有一个通道在你注册的事件上就绪了
- select (long timeout) 最长会阻塞 timeout 毫秒 (参数)
- selectNow () 不会阻塞,不管什么通道就绪都立刻返回
- 访问已选择键集
- 一旦调用了 select () 方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用 selector 的 selectedKeys () 方法,访问 “已选择键集(selected key set)
- selectedKeys()
Set selectedKeys = selector.selectedKeys();
# NIO 结构
Error: Navigating frame was detached
//Server | |
//Selector 构建 | |
Selector selector = Selector.open(); | |
//Channel 构建 | |
ServerSocketChannel servChannel = ServerSocketChannel.open(); | |
servChannel.configureBlocking(false); // 设置非阻塞模式 | |
servChannel.socket().bind(new InetSocketAddress(8001), 1024);// 设定 8001 端口 | |
servChannel.register(selector, SelectionKey.OP_ACCEPT); //selector 绑定 | |
//selector 一次遍历所有 channel | |
selector.select(1000); // 最多等待堵塞 1000ms | |
Set<SelectionKey> selectedKeys = selector.selectedKeys(); // 获得所有有数据响应的 key | |
Iterator<SelectionKey> it = selectedKeys.iterator(); | |
while (it.hasNext()) { | |
SelectionKey key = it.next(); | |
it.remove(); | |
try { | |
//Handle | |
} catch (Exception e) { | |
if (key != null) { | |
key.cancel(); | |
if (key.channel() != null) | |
key.channel().close(); | |
} | |
} | |
} | |
//Handle | |
if(key.isValid()) { | |
if(key.isAcceptable()) { | |
// Accept the new connection | |
ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); | |
SocketChannel sc = ssc.accept(); | |
sc.configureBlocking(false); | |
// Add the new connection to the selector | |
sc.register(selector, SelectionKey.OP_READ); | |
} | |
if(key.isReadable()) { | |
//Read the data | |
} | |
} | |
//Client | |
//Selector 构建 | |
Selector selector = Selector.open(); | |
//Channel 构建 | |
SocketChannel socketChannel = SocketChannel.open(); | |
socketChannel.configureBlocking(false); // 设置非阻塞模式 | |
// 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答 | |
if (socketChannel.connect(new InetSocketAddress("127.0.0.1", 8001))) { | |
socketChannel.register(selector, SelectionKey.OP_READ); | |
//doWrite | |
} else { | |
socketChannel.register(selector, SelectionKey.OP_CONNECT); | |
} | |
//Handle | |
if(key.isValid()) { | |
// 判断是否连接成功 | |
SocketChannel sc = (SocketChannel) key.channel(); | |
if(key.isConnectable()) { | |
if (sc.finishConnect()) { | |
sc.register(selector, SelectionKey.OP_READ); | |
} | |
} | |
if(key.isReadable()) { | |
//Read the data | |
} | |
} | |
//doWrite | |
byte[] str = //TODO | |
ByteBuffer writeBuffer = ByteBuffer.allocate(str.length); //Buffer 分配内存 | |
writeBuffer.put(str); //str 写入 Buffer | |
writeBuffer.flip(); // 读写模式切换 | |
sc.write(writeBuffer); //Buffer 数取数据到 Channel |
# 2.AIO 编程
- Asynchronous I/O, 异步 I/O
- JDK 1.7 引入,主要在 java.nio 包中
- 异步 I/O,采用回调方法进行处理读写操作
# 主要类
- AsynchronousServerSocketChannel 服务器接受请求通道
- bind 绑定在某一个端口 accept 接受客户端请求
- AsynchronousSocketChannel Socket 通讯通道
- read 读数据 write 写数据
- CompletionHandler 异步处理类
- completed 操作完成后异步调用方法 failed 操作失败后异步调用方法
// 服务器通道建立 | |
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(); | |
server.bind(new InetSocketAddress("localhost", 8001)); | |
// 服务器接受客户端请求,成功接收后自动回调 | |
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { | |
@Override | |
public void completed(AsynchronousSocketChannel channel, Object attachment) { | |
//TODO | |
} | |
@Override | |
public void failed(Throwable exc, Object attachment) { | |
//TODO | |
} | |
}); | |
// 客户端通道建立 | |
AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(); | |
// 连接成功后自动回调 | |
channel.connect(new InetSocketAddress("localhost", 8001), null, new CompletionHandler<Void, Void>() { | |
@Override | |
public void completed(Integer result, Object attachment) { | |
//TODO | |
} | |
@Override | |
public void failed(Throwable exc, Object attachment) { | |
//TODO | |
} | |
}); |
# 三种 I/O 的区别
- | BIO | NIO | AIO |
---|---|---|---|
阻塞方式 | 阻塞 | 非阻塞 | 非阻塞 |
同步方式 | 同步 | 同步 | 异步 |
编程难度 | 简单 | 较难 | 困难 |
客户机 / 服务器线程对比 | 1:1 | N:1 | N:1 |
性能 | 低 | 高 | 高 |
# 3.Netty 编程
# 关键技术
- 通道 Channel
- ServerSocketChannel/NioServerSocketChannel/…
- SocketChannel/NioSocketChannel
- 事件 EventLoop
- 为每个通道定义一个 EventLoop,处理所有的 I/O 事件
- EventLoop 注册事件
- EventLoop 将事件派发给 ChannelHandler
- EventLoop 安排进一步操作
- 事件
- 事件按照数据流向进行分类
- 入站事件:连接激活 / 数据读取 /……
- 出站事件:打开到远程连接 / 写数据 /……
- 事件处理 ChannelHandler
- Channel 通道发生数据或状态改变
- EventLoop 会将事件分类,并调用 ChannelHandler 的回调函数
- 程序员需要实现 ChannelHandler 内的回调函数
- ChannelInboundHandler/ChannelOutboundHandler
- ChannelHandler 工作模式:责任链
- 责任链模式
- 将请求的接收者连成一条链
- 在链上传递请求,直到有一个接收者处理该请求
- 避免请求者和接收者的耦合
- ChannelHandler 可以有多个,依次进行调用
- ChannelPipeline 作为容器,承载多个 ChannelHandler
- 责任链模式
- ByteBuf
- 强大的字节容器,提供丰富 API 进行操作
//from BiliBili | |
//Server | |
EventLoopGroup bossGroup = new NioEventLoopGroup(); | |
EventLoopGroup workGroup = new NioEventLoopGroup(); | |
ServerBootstrap bootstrap = new ServerBootstrap(); | |
// 链式编程,设置服务器 | |
bootstrap.group(bossGroup,workGroup) // 设置两个线程组 | |
.channel(NioServerSocketChannel.class) // 使用 NioServerSocketChannel 作为服务器通道的实现 | |
.option(ChannelOption.SO_BACKLOG,128) // 设置线程队列得到连接个数 | |
.childOption(ChannelOption.SO_KEEPALIVE,true) // 设置保持活动连接状态 | |
.childHandler(new ChannelInitializer<SocketChannel>() { // 给 workGroup 的 EventLoop 对应的管道设置处理器 | |
@Override | |
public void initChannel(SocketChannel ch) throws Exception { | |
ch.pipeline().addLast(new ServerHandler()); // 添加处理类 | |
} | |
}); | |
// 绑定一个端口并同步,生成了一个 ChannelFuture 对象 | |
ChannelFuture cf =bootstrap.bind(6668).sync(); | |
// 对关闭通道进行监听 | |
cf.channel().closeFuture().sync(); | |
//ServerHandler extends ChannelInboundHandlerAdapter | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception{ | |
ByteBuf in = (ByteBuf) msg; | |
String content = in.toString(CharsetUtil.UTF_8); | |
System.out.println("Server received: " + content); | |
System.out.println("Client address: "+ctx.channel().remoteAddress()); | |
ByteBuf out = ctx.alloc().buffer(1024); | |
out.writeBytes((content + " 666").getBytes()); | |
ctx.write(out); | |
} | |
@Override | |
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { | |
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) | |
.addListener(ChannelFutureListener.CLOSE); | |
} | |
@Override | |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { | |
cause.printStackTrace(); | |
ctx.close(); | |
} | |
//Client | |
EventLoopGroup eventExecutors = new NioEventLoopGroup(); | |
try { | |
Bootstrap bootstrap = new Bootstrap(); | |
bootstrap.group(eventExecutors) // 设置两个线程组 | |
.channel(NioSocketChannel.class) // 使用 NioSocketChannel 作为服务器通道的实现 | |
.handler(new ChannelInitializer<SocketChannel>() { // 给 eventExecutors 的 EventLoop 对应的管道设置处理器 | |
@Override | |
public void initChannel(SocketChannel ch) throws Exception { | |
ch.pipeline().addLast(new ClientHandler()); // 添加处理类 | |
} | |
}); | |
System.out.println("...客户端 is ready..."); | |
// 启动客户端连接服务端 | |
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",6668).sync(); | |
// 给关闭通道进行监听 | |
channelFuture.channel().closeFuture().sync(); | |
}finally { | |
// 优雅关闭 | |
eventExecutors.shutdownGracefully(); | |
} | |
//ClientHandler extends ChannelInboundHandlerAdapter | |
@Override | |
public void channelActive(ChannelHandlerContext ctx) throws Exception { | |
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8)); | |
} | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) { | |
ByteBuf buf = (ByteBuf) msg; | |
System.out.println("Client received: " + buf.toString(CharsetUtil.UTF_8)); | |
System.out.println("Server address: "+ctx.channel().remoteAddress()); | |
} | |
@Override | |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { | |
cause.printStackTrace(); | |
ctx.close(); | |
} |
# 4. 邮件基础知识
# 主要协议 (发送端口 25, 接收端口 110)
- 发送,SMTP, Simple Mail Transfer Protocol
- 接收,Pop3, Post Office Protocol 3, (POP)
- 接收,IMAP, Internet Message Access Protocol, IMAP4
- 摘要浏览
- 选择下载附件
- 多文件夹
- 网络硬盘
# 5.Mail 编程
# 邮件服务器支持
- 需要在邮件服务内设置,可以查看相关邮件帮助
- 需要知道 pop 服务器和 smtp 服务器信息
# javax.mail 包和 javax.mail.internet 包
# 关键类
- Session: 邮件会话 和 HttpSession 不同
- Store: 邮件存储空间
- Folder: 邮件文件夹
- Message: 电子邮件
- Address: 邮件地址
- Transport: 发送协议类
//Receive | |
public class MailClientRecv { | |
private Session session; | |
private Store store; | |
private String username = "chenliangyu1980@126.com"; | |
private String password = "1234567899"; | |
private String popServer = "pop.126.com"; | |
public void init()throws Exception | |
{ | |
// 设置属性 | |
Properties props = new Properties(); | |
props.put("mail.store.protocol", "pop3"); | |
props.put("mail.imap.class", "com.sun.mail.imap.IMAPStore"); | |
props.put("mail.pop3.class", "com.sun.mail.pop3.POP3Store"); | |
// 创建 Session 对象 | |
session = Session.getInstance(props,null); | |
session.setDebug(false); // 输出跟踪日志 | |
// 创建 Store 对象 | |
store = session.getStore("pop3"); | |
// 连接到收邮件服务器 | |
store.connect(popServer,username,password); | |
} | |
public void receiveMessage()throws Exception | |
{ | |
String folderName = "inbox"; | |
Folder folder=store.getFolder(folderName); | |
if(folder==null) | |
{ | |
throw new Exception(folderName+"邮件夹不存在"); | |
} | |
// 打开信箱 | |
folder.open(Folder.READ_ONLY); | |
System.out.println("您的收件箱有"+folder.getMessageCount()+"封邮件."); | |
System.out.println("您的收件箱有"+folder.getUnreadMessageCount()+"封未读的邮件."); | |
// 读邮件 | |
Message[] messages=folder.getMessages(); | |
//for(int i=1;i<=messages.length;i++) | |
for(int i=1;i<=3;i++) | |
{ | |
System.out.println("------第"+i+"封邮件-------"); | |
// 打印邮件信息 | |
Message message = messages[i]; | |
//folder.getMessage(i).writeTo(System.out); | |
System.out.println((message.getFrom())[0]); | |
System.out.println(message.getSubject()); | |
System.out.println(); | |
} | |
folder.close(false); // 关闭邮件夹 | |
} | |
public void close()throws Exception | |
{ | |
store.close(); | |
} | |
public static void main(String[] args)throws Exception { | |
MailClientRecv client=new MailClientRecv(); | |
// 初始化 | |
client.init(); | |
// 接收邮件 | |
client.receiveMessage(); | |
// 关闭连接 | |
client.close(); | |
} | |
} | |
//Send | |
public class MailClientSend { | |
private Session session; | |
private Transport transport; | |
private String username = "lychen@sei.ecnu.edu.cn"; | |
private String password = "1234567899"; | |
private String smtpServer = "webmail.ecnu.edu.cn"; | |
public void init()throws Exception | |
{ | |
// 设置属性 | |
Properties props = new Properties(); | |
props.put("mail.transport.protocol", "smtp"); | |
props.put("mail.smtp.class", "com.sun.mail.smtp.SMTPTransport"); | |
props.put("mail.smtp.host", smtpServer); // 设置发送邮件服务器 | |
props.put("mail.smtp.port", "25"); | |
props.put("mail.smtp.auth", "true"); //SMTP 服务器需要身份验证 | |
// 创建 Session 对象 | |
session = Session.getInstance(props,new Authenticator(){ // 验账账户 | |
public PasswordAuthentication getPasswordAuthentication() { | |
return new PasswordAuthentication(username, password); | |
} | |
}); | |
session.setDebug(true); // 输出跟踪日志 | |
// 创建 Transport 对象 | |
transport = session.getTransport(); | |
} | |
public void sendMessage()throws Exception{ | |
// 创建一个邮件 | |
//Message msg = TextMessage.generate(); | |
//Message msg = HtmlMessage.generate(); | |
Message msg = AttachmentMessage.generate(); | |
// 发送邮件 | |
transport.connect(); | |
transport.sendMessage(msg, msg.getAllRecipients()); | |
// 打印结果 | |
System.out.println("邮件已经成功发送"); | |
} | |
public void close()throws Exception | |
{ | |
transport.close(); | |
} | |
public static void main(String[] args)throws Exception { | |
MailClientSend client=new MailClientSend(); | |
// 初始化 | |
client.init(); | |
// 发送邮件 | |
client.sendMessage(); | |
// 关闭连接 | |
client.close(); | |
} | |
} | |
//Text | |
String from = "lychen@sei.ecnu.edu.cn "; // 发件人地址 | |
String to = "chenliangyu1980@126.com"; // 收件人地址 | |
String subject = "test"; | |
String body = "您好,这是来自一封chenliangyu的测试邮件"; | |
// 创建 Session 实例对象 | |
Session session = Session.getDefaultInstance(new Properties()); | |
// 创建 MimeMessage 实例对象 | |
MimeMessage message = new MimeMessage(session); | |
// 设置发件人 | |
message.setFrom(new InternetAddress(from)); | |
// 设置收件人 | |
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); | |
// 设置发送日期 | |
message.setSentDate(new Date()); | |
// 设置邮件主题 | |
message.setSubject(subject); | |
// 设置纯文本内容的邮件正文 | |
message.setText(body); | |
// 保存并生成最终的邮件内容 | |
message.saveChanges(); | |
return message; | |
//Html | |
String from = "lychen@sei.ecnu.edu.cn "; // 发件人地址 | |
String to = "chenliangyu1980@126.com"; // 收件人地址 | |
String subject = "HTML邮件"; | |
String body = "<a href=http://www.ecnu.edu.cn>" | |
+ "<h4>欢迎大家访问我们的网站</h4></a></br>" | |
+ "<img src=\"https://news.ecnu.edu.cn/_upload/article/images/2e/e2/6b554d034c9192101208c732195e/16a6ec66-6729-4469-a5f4-0435e0f2e66a.jpg\">"; | |
// 创建 Session 实例对象 | |
Session session = Session.getDefaultInstance(new Properties()); | |
// 创建 MimeMessage 实例对象 | |
MimeMessage message = new MimeMessage(session); | |
// 设置发件人 | |
message.setFrom(new InternetAddress(from)); | |
// 设置收件人 | |
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); | |
// 设置发送日期 | |
message.setSentDate(new Date()); | |
// 设置邮件主题 | |
message.setSubject(subject); | |
// 设置 HTML 格式的邮件正文 | |
message.setContent(body, "text/html;charset=gb2312"); | |
// 保存并生成最终的邮件内容 | |
message.saveChanges(); | |
return message; | |
//Attachment | |
String from = "lychen@sei.ecnu.edu.cn "; // 发件人地址 | |
String to = "chenliangyu1980@126.com"; // 收件人地址 | |
String subject = "多附件邮件"; // 邮件主题 | |
String body = "<a href=http://www.ecnu.edu.cn>" + | |
"欢迎大家访问我们的网站</a></br>"; | |
// 创建 Session 实例对象 | |
Session session = Session.getDefaultInstance(new Properties()); | |
// 创建 MimeMessage 实例对象 | |
MimeMessage message = new MimeMessage(session); | |
message.setFrom(new InternetAddress(from)); | |
message.setRecipients(Message.RecipientType.TO, | |
InternetAddress.parse(to)); | |
message.setSubject(subject); | |
// 创建代表邮件正文和附件的各个 MimeBodyPart 对象 | |
MimeBodyPart contentPart = createContent(body); | |
MimeBodyPart attachPart1 = createAttachment("c:/temp/ecnu4.jpg"); | |
MimeBodyPart attachPart2 = createAttachment("c:/temp/ecnu5.jpg"); | |
// 创建用于组合邮件正文和附件的 MimeMultipart 对象 | |
MimeMultipart allMultipart = new MimeMultipart("mixed"); | |
allMultipart.addBodyPart(contentPart); | |
allMultipart.addBodyPart(attachPart1); | |
allMultipart.addBodyPart(attachPart2); | |
// 设置整个邮件内容为最终组合出的 MimeMultipart 对象 | |
message.setContent(allMultipart); | |
message.saveChanges(); | |
return message; | |
public static MimeBodyPart createContent(String body) throws Exception { | |
MimeBodyPart htmlBodyPart = new MimeBodyPart(); | |
htmlBodyPart.setContent(body,"text/html;charset=gb2312"); | |
return htmlBodyPart; | |
} | |
public static MimeBodyPart createAttachment(String filename) throws Exception { | |
// 创建保存附件的 MimeBodyPart 对象,并加入附件内容和相应信息 | |
MimeBodyPart attachPart = new MimeBodyPart(); | |
FileDataSource fds = new FileDataSource(filename); | |
attachPart.setDataHandler(new DataHandler(fds)); | |
attachPart.setFileName(fds.getName()); | |
return attachPart; | |
} |