TCP/IP学习笔记一:BIO的网络编程-简单实例
标签(空格分隔): BIO 网络编程
一、简单的BIO网络编程
实例:实现简单的web服务器(简单模仿Tomcat的请求和响应)
服务器端的实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
System.out.println("监听端口为8989的服务器");
//3.获取报文
Socket socket = serverSocket.accept();
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
//关闭流
br.close();
pw.close();
socket.close();
}
}
客户端的实现:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客户端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//1.创建Socket
Socket socket = new Socket();
//2.连接服务器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.发送数据到服务器
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("这是客户端数据");
//将数据发送
pw.flush();
//告知服务器已经到了流的结尾
socket.shutdownOutput();
//b.获得服务器的回应
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
//打印服务器回应数据
System.out.println(sb.toString());
//关闭流
br.close();
pw.close();
socket.close();
}
}
运行服务器端:
监听端口为8989的服务器
运行客户端:
接收的数据:这是客户端数据 服务器已经接收到数据
运行客户端之后服务器端结果为:
监听端口为8989的服务器
服务器收到的数据:这是客户端数据
到此服务器端和客户端运行结束。这样的结果并和我们想要的结果不同,我们需要的是服务器端一直监听客户端发送的请求并作出响应。我们【服务器端】做如下的修改,客户端代码不做任何修改。
服务器端修改为:
在执行的代码中加上while(true)【第180行】,保证执行代码一直执行。
package com.bio.socket;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
while(true){
System.out.println("监听端口为8989的服务器");
//3.获取报文
Socket socket = serverSocket.accept();
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
//关闭流
br.close();
pw.close();
socket.close();
}
}
}
运行服务器端:
监听端口为8989的服务器
运行客户端:
接收的数据:这是客户端数据 服务器已经接收到数据
运行客户端之后服务器端结果为:
监听端口为8989的服务器
服务器收到的数据:这是客户端数据
监听端口为8989的服务器
到此服务器端和客户端已经执行结束。也达到了一直监听客户端的请求的目的,但是还是有问题:我们处理客户端的请求的时候会存在一个请求没有处理完成,另一个请求不能处理。(在处理的过程中最耗时的操作就是I/O操作)。为了解决这个问题,我们进行如下修改。
服务器端代码修改为:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
while(true){
System.out.println("监听端口为8989的服务器");
//3.获取报文
final Socket socket = serverSocket.accept();
//启动线程
new Thread(){
public void run() {
try {
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
//关闭流
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};
}.start();
}
}
}
客户端代码修改为:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
/**
* 客户端
* @author MOTUI
*
*/
public class BIOClientSocket {
public static void main(String[] args) throws Exception {
//使用线程模拟用户 并发访问
for (int i = 0; i < 20; i++) {
new Thread(){
public void run() {
try {
//1.创建Socket
Socket socket = new Socket();
//2.连接服务器
socket.connect(new InetSocketAddress("192.168.0.117",8989));
//a.发送数据到服务器
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("这是客户端数据");
//将数据发送
pw.flush();
//告知服务器已经到了流的结尾
socket.shutdownOutput();
//b.获得服务器的回应
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
//打印服务器回应数据
System.out.println(sb.toString());
//关闭流
br.close();
pw.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
};
}.start();
}
}
}
运行服务器端:
监听端口为8989的服务器
运行客户端:
接收的数据:这是客户端数据 服务器已经接收到数据
....(省略19个:监听端口为8989的服务器)
运行客户端之后服务器端结果为:
监听端口为8989的服务器
....(省略17个:监听端口为8989的服务器)
监听端口为8989的服务器
当前线程ID:9
监听端口为8989的服务器
当前线程ID:10
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:11
当前线程ID:12
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:13
服务器收到的数据:这是客户端数据
当前线程ID:14
服务器收到的数据:这是客户端数据
当前线程ID:15
当前线程ID:17
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:18
当前线程ID:21
当前线程ID:20
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:23
当前线程ID:22
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:24
当前线程ID:25
当前线程ID:26
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:28
当前线程ID:27
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:16
当前线程ID:19
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
到此运行结束,运行结果在不同的机器上有不同的顺序这与机器的运行效率有关。分析到这里我们感觉这种方式已经很合理了。在了解高并发之前,这的确看着不错。但是我们可以思考一个问题:如果我们的并发打到万级或者百万级或者更高的时候,我们的程序有没有问题呢?我们每有一个请求就创建一个线程,而且我们创建线程之后并没有关注这个线程是否正确执行?是否执行结束?我们都没有关注,这时候就出现问题了,当高并发情况下有很多的线程处于阻塞状态,而我们的系统资源已经占用,系统对I/O的处理就会慢,对I/O的处理变慢就会导致我们的线程阻塞,恶性循环直到系统假死(宕机)。这样的处理是不是不合理呢。我们现在虽然无法做到线程不阻塞(不阻塞就是NIO的方式),但是我们可以进行其他的做法,我们可以控制线程的创建个数,这样就会相对打到一个平衡。
对于我们如何控制线程创建的个数呢?我们可能会想到用计数器,javaAPI中提供了线程池的方式,使用更简单。代码:
服务器端修改为:
package com.bio.socket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 服务器端
* @author MOTUI
*
*/
public class BIOServerSocket {
public static void main(String[] args) throws Exception {
//1.创建serverSocket
ServerSocket serverSocket = new ServerSocket();
//2.设置端口号
serverSocket.bind(new InetSocketAddress(8989));
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
while(true){
System.out.println("监听端口为8989的服务器");
//3.获取报文
final Socket socket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
//打印当前线程ID
System.out.println("当前线程ID:"+Thread.currentThread().getId());
//a.获得客户端的意图 request
InputStream is = socket.getInputStream();
//字符流和字节流的转换
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
//存储读取的数据
StringBuilder sb = new StringBuilder();
String line = null;
//读取数据
while((line = br.readLine()) != null){
//将数据存储在StringBuilder中
sb.append(line);
}
System.out.println("服务器收到的数据:"+sb.toString());
//b.返回客户端数据
OutputStream os = socket.getOutputStream();
//获得输出流
PrintWriter pw = new PrintWriter(os);
pw.write("接收的数据:"+sb.toString()+" 服务器已经接收到数据");
//将数据发送
pw.flush();
//关闭流
br.close();
pw.close();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}
运行服务器端:
监听端口为8989的服务器
运行客户端:
接收的数据:这是客户端数据 服务器已经接收到数据
....(省略19个:监听端口为8989的服务器)
运行客户端之后服务器端结果为:
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
当前线程ID:9
监听端口为8989的服务器
当前线程ID:10
监听端口为8989的服务器
监听端口为8989的服务器
当前线程ID:12
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:13
服务器收到的数据:这是客户端数据
监听端口为8989的服务器
当前线程ID:11
监听端口为8989的服务器
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:14
监听端口为8989的服务器
监听端口为8989的服务器
服务器收到的数据:这是客户端数据
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
监听端口为8989的服务器
当前线程ID:14
服务器收到的数据:这是客户端数据
当前线程ID:9
当前线程ID:13
当前线程ID:14
服务器收到的数据:这是客户端数据
当前线程ID:10
当前线程ID:12
服务器收到的数据:这是客户端数据
当前线程ID:9
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:12
当前线程ID:10
服务器收到的数据:这是客户端数据
当前线程ID:9
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:15
当前线程ID:17
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
当前线程ID:18
当前线程ID:16
服务器收到的数据:这是客户端数据
服务器收到的数据:这是客户端数据
到此我们的程序就算相对完善的。在服务器端我们设置了线程的最大数为10,所以我们的程序最大并发量为10,这样我们的程序就不需要一直新建线程浪费资源,只需要等待别人用完还回线程池,然后拿到继续使用即可。但是我们的程序依然是线程阻塞的。
总结:
对于BIO的网络编程,我们实现的是传输层的TCP/IP协议,这样的传输效率更高。对于webService的SOP协议是Http+XML,过度的封装使得效率上不适合对速度要求较高的应用。
多线程(线程池)实现BIO并非访问,将IO的处理交给线程池里的线程,限定系统线程无止境的创建节省系统资源实现线程复用.
缺点: 线程会因为IO没有就绪挂起.
问题分析:
上述程序属于伪异步I/O,存在什么样的弊端?
/**
* Reads some number of bytes from the input stream and stores them into
* the buffer arrayb
. The number of bytes actually read is
* returned as an integer. **This method blocks until input data is
* available, end of file is detected, or an exception is thrown.**
*
*If the length of
b
is zero, then no bytes are read and
*0
is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at the
* end of the file, the value-1
is returned; otherwise, at
* least one byte is read and stored intob
.
* ……..
*/
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
上文中的粗体字对Socket的读操作的说明,当Socket的输入流进行读操作的时候,它会一致阻塞下去,知道发生三件事:
- 有数据可读
- 可用数据已经读完
- 发生空指针或者I/O异常
这样就意味着当发送方请求说这响应消息较慢,或者网络传输较慢,读取输入流的一方的通信线程将会阻塞很长时间,如果对方60s发送完数据,读入一方的通信线程将阻塞60s,在此期间其他接入消息将在消息队列中排队.