Socket 编程
Internet中应用嘴广泛的网络应用编程接口实现与3种底层协议接口:
- 数据报类型套接字SCOK_DGRAM(面向UDP接口)
- 流式套接字SOCK_STREAM(面向TCP接口)
- 原始套接字SOCK_RAW(面向网络层协议接口IP、ICMP等)
主要socket API及其调用过程 :
创建套接字 --> 端点绑定 --> 发送数据 --> 接收数据 --> 释放套接字
Socket API 函数定义
listen()、accept()函数只能用于服务器端;
connect()函数只能用于客户端;
socket()、bind()、send()、recv()、sendto()、recvfrom()、close() 客户端和服务端通用。
BIO和NIO
- 阻塞(blocking)IO: 资源不可用时,IO请求一直阻塞,直到反馈结果(游数据或者超时)。
- 非阻塞(non-blocking)IO :资源不可用时,IO请求离开返回,返回数据标志资源不可用。
- 同步(synchronous)IO:应用阻塞在发送或接收数据状态,直到数据成功传输或失败返回。
- 异步(asynchronous)IO:应用发送或接收到数据后立刻返回,实际处理是异步执行的。
阻塞和非阻塞是获取资源的方式,同步/异步是程序设计如何处理资源的逻辑设计。代码中使用的API:ServerSocket#accept、InputStream#read都是阻塞API。操作系统底层API中,默认使用操作都是Blocking型,send/recv等接口都是阻塞的。
BIO
阻塞I/O 模型
- 阻塞I/O(blocking I/O)模型,进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回,阻塞导致在处理网络I/O时,一个线程只能处理一个网络连接,造成资源极大的浪费。
传统Socket阻塞案例代码
网络编程的基本模型是C/S模型,即两个进程间的通信。
服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。
传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。
简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。
public class BIOClient {
private static Charset charset = Charset.forName("UTF-8");
public static void main(String[] args) throws Exception {
Socket s = new Socket("localhost", 8080);
OutputStream out = s.getOutputStream();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入:");
String msg = scanner.nextLine();
out.write(msg.getBytes(charset)); // 阻塞,写完成
scanner.close();
s.close();
}
}
public class BIOServer{
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动成功");
while (!serverSocket.isClosed()) {
Socket request = serverSocket.accept();// 阻塞
System.out.println("收到新连接 : " + request.toString());
try {
// 接收数据、打印
InputStream inputStream = request.getInputStream(); // net + i/o
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String msg;
while ((msg = reader.readLine()) != null) {
// 没有数据,阻塞
if (msg.length() == 0) {
break;
}
System.out.println(msg);
}
System.out.println("收到数据,来自:"+ request.toString());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
request.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
serverSocket.close();
}
}
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了。
伪异步I/O
为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程,实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型”。
private static ExecutorService threadPool = Executors.newCachedThreadPool();
public