(2)网络编程
1 什么是Socket,交互过程是怎么样的?
Socket 的中文意思是电源插座、电器插口、插孔。通过 Socket 可以将两台计算机连接起来,建立连接的过程相当于插座插入插槽。Socket 是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。Socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。
Socket 的交互过程:
(1)服务端初始化Socket实例化一个类拿到对象,然后绑定IP端口,监听客户端,接收外部连接。
(2)客户端初始化一个socket,然后connect与服务端建立好双向链接与accept对应。
(3)客户端发送请求数据,服务端处理请求并给客户端回应数据,这样一个通信循环。
(4)客户端关闭 Socket,一次交互结束。
2 如何用代码实现TCP Socket通信?
TCP协议Socket使用BIO进行通信,服务端代码如下:
package com.test.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class BIOServer {
// 在main线程中执行下面这些代码
public static void main(String[] args) {
//1单线程服务
ServerSocket server = null;
Socket socket = null;
InputStream in = null;
OutputStream out = null;
try {
server = new ServerSocket(8000);
System.out.println("服务端启动成功,监听端口为8000,等待客户端连接...");
while (true){
socket = server.accept(); //等待客户端连接
System.out.println("客户连接成功,客户信息为:" +
socket.getRemoteSocketAddress());
in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = 0;
//读取客户端的数据
while ((len = in.read(buffer)) > 0) {
System.out.println(new String(buffer, 0, len));
}
//向客户端写数据
out = socket.getOutputStream();
out.write("hello!".getBytes());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
TCP协议Socket使用BIO进行通信,客户端代码如下:
package com.test.io;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
public class Client01 {
public static void main(String[] args) throws IOException {
//创建套接字对象socket并封装ip与port
Socket socket = new Socket("127.0.0.1", 8000);
//根据创建的socket对象获得一个输出流
OutputStream outputStream = socket.getOutputStream();
//控制台输入以IO的形式发送到服务器
System.out.println("TCP连接成功 \n请输入:");
while(true){
byte[] car = new Scanner(System.in).nextLine().getBytes();
outputStream.write(car);
System.out.println("TCP协议的Socket发送成功");
//刷新缓冲区
outputStream.flush();
}
}
}
3 如何用代码实现 Socket 双向通信 ?
双向通信,发送消息并接受消息,服务端代码如下:
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);
// server将一直等待连接的到来
System.out.println("server将一直等待连接的到来");
Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
//只有当客户端关闭它的输出流的时候,服务端才能取得结尾的-1
while ((len = inputStream.read(bytes)) != -1) {
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len, "UTF-8"));
}
System.out.println("get message from client: " + sb);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello Client,I get the message.".getBytes("UTF-8"));
inputStream.close();
outputStream.close();
socket.close();
server.close();
}
}
服务端读取完客户端的消息后,打开输出流,将指定消息发送回客户端,客户端程序为:
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class SocketClient {
public static void main(String args[]) throws Exception {
// 要连接的服务端IP地址和端口
String host = "127.0.0.1";
int port = 55533;
// 与服务端建立连接
Socket socket = new Socket(host, port);
// 建立连接后获得输出流
OutputStream outputStream = socket.getOutputStream();
String message = "你好 yiwangzhibujian";
socket.getOutputStream().write(message.getBytes("UTF-8"));
//通过shutdownOutput高速服务器已经发送完数据,后续只能接受数据
socket.shutdownOutput();
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
//指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len,"UTF-8"));
}
System.out.println("get message from server: " + sb);
inputStream.close();
outputStream.close();
socket.close();
}
}
4 通过线程池如何优化上面的服务端程序?
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SocketServer {
public static void main(String args[]) throws Exception {
// 监听指定的端口
int port = 55533;
ServerSocket server = new ServerSocket(port);
// server将一直等待连接的到来
System.out.println("server将一直等待连接的到来");
//如果使用多线程,那就需要线程池,防止并发过高时创建过多线程耗尽资源
ExecutorService threadPool = Executors.newFixedThreadPool(100);
while (true) {
Socket socket = server.accept();
Runnable runnable=()->{
try {
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
StringBuilder sb = new StringBuilder();
while ((len = inputStream.read(bytes)) != -1) {
// 注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8
sb.append(new String(bytes, 0, len, "UTF-8"));
}
System.out.println("get message from client: " + sb);
inputStream.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
};
threadPool.submit(runnable);
}
}
}
5 说说select、poll、epoll的区别?
select、poll、epoll是实现IO多路复用的主要机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪,一般是读就绪或者写就绪,能够通知程序进行相应的读写操作。
- select
select将所要进行IO操作管理的文件描述法fd放在一个集合中。通过轮询的机制遍历组内所有的fd哪些已经准备好了,再对已经准备好的fd执行相应的IO操作。select的缺点很明显,首先这个集合所能管理的文件描述符的数目有限最大为1024,当集合中文件描述符的数目增多时,轮询的方式就及其浪费时间,极大的降低了效率,而且每次调用select需要将该集合中的文件描述符进行从用户空间到内核空间的拷贝工作。
- poll
poll和select比较相似,也是需要以轮询的方式去遍历所有监听的文件描述符,但是比select好的是poll没有集合数目的限制,一般情况只要内存空间足够大就可以足够多。poll也是需要完成大量文件描述符的数组在用户空间到内核空间的拷贝,增大了系统开销,并且随着文件描述符的增多而线性增加。
- epoll
epoll在select和poll上做了很大的改进:
(1)epoll所能监听的文件描述符的数目上限是最大能打开文件的数目。
(2)select和poll都只提供了一个函数:select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。 每当有新的事件要注册到epoll句柄时,才会将所有的文件描述符拷贝到内核,而不是像select和poll一样,每次epoll_wait的时候进行重复拷贝, 这样减少了大量用户态到内核态的拷贝工作,从而大大节省了系统开销。
(3)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
6 说说 Netty 的线程模型?
- 单线程模型
一个线程需要处理所有的 accept、read、decode、process、encode、send 事件。对于高负载、高并发、性能要求高的场景不适用。使用 NioEventLoopGroup 类的无参构造函数设置线程数量的默认值就是 CPU 核心数 ,代码如下:
//1.eventGroup既用于处理客户端连接,又负责具体的处理。
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap boobtstrap = new ServerBootstrap();
boobtstrap.group(eventGroup, eventGroup)
// do something
- Reactor多线程模型
一个 Acceptor 线程只负责监听客户端的连接,一个 NIO 线程池负责具体处理:accept、read、decode、process、encode、send事件。适合并发连接数较小的情况,满足绝大部分应用场景,代码如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap boobtstrap = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
boobtstrap.group(bossGroup, workerGroup)
// do something
- Reactor主从多线程模型
Reactor主从多线程模型用于解决Reactor多线程模型中一个Acceptor性能不足的情况,它的特点是:
(1)服务端用于接受客户端连接的是一个独立的NIO线程池。
(2)Acceptor接收到客户端TCP连接请求,并处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池( Reactor Pool)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。
(3)Acceptor线程池(NIO线程池)仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
参考
https://blog.csdn.net/u013310119/article/details/102609950
https://www.cnblogs.com/1130136248wlxk/articles/5224203.html
https://blog.51cto.com/u_12482901/5715521
https://developer.aliyun.com/article/858795
https://blog.csdn.net/BASK2311/article/details/128404551
https://www.jianshu.com/p/3e98338cd359