1. 简介
BIO 全称 Blocking IO , 也就是阻塞IO,这里的阻塞体现在两个方面,
- accept内核分配的Socket资源后,阻塞的等待客户端的连接
- 读取客户端发送的数据时是阻塞的
2. 笔者环境
- Ubuntu 18.04
- JDK1.4
3. 安装JDK1.4
注意这里要演示原始的BIO模型,需要使用JDK1.4来编译;如果使用JDK8编译,会发现在系统调用层面使用的是poll模型
- https://www.oracle.com/java/technologies/java-archive-javase-v14-downloads.html
- chmod a+x j2sdk-1_4_2_19-linux-i586.bin
- ./j2sdk-1_4_2_19-linux-i586.bin
- 如果报 ./install.sfx.5628: not found 这个错误
- 需要执行sudo apt-get install g+±multilib
4. 示例代码
- vim SocketBIO.java
- j2sdk1.4.2_19/bin/javac SocketBIO.java
- strace -ff -o out j2sdk1.4.2_19/bin/java SocketBIO
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9090,20);
System.out.println("create ServerSocket");
while (true) {
final Socket client = server.accept(); //阻塞1
System.out.println("client " + client.getPort());
new Thread(new Runnable(){
public void run() {
InputStream in = null;
try {
in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while(true){
String dataline = reader.readLine(); //阻塞2
if(null != dataline){
System.out.println(dataline);
}else{
client.close();
break;
}
}
System.out.println("client close");
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
- 这里strace 是 查看系统调用命令,会将结果输出到out为前缀的文件中,后面的id是线程id
- 可以看到此时控制台输出
$ strace -ff -o out j2sdk1.4.2_19/bin/java SocketBIO
create ServerSocket
5. 查看系统调用
- 打开一个新的命令行,查看系统调用日志,查找输出 create ServerSocket 的文件
$ grep ServerSocket out.*
out.2350:write(1, "create ServerSocket", 19) = 19
- 打开out.2350文件,查找上面 write(1, “create ServerSocket”, 19)这一行
- vim out.2350
- 在vim中输入 / 就可以查找,然后输入 ServerSocket就可以查到了
socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 3 ##返回的文件描述符是3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0)}, 24) = 0
listen(3, 20) = 0
write(1, "create ServerSocket", 19) = 19 ##返回的是字符长度
write(1, "\n", 1) = 1
accept(3, ## 阻塞了,没有返回值
- 可以发现Socket 的创建过程使用了四个关键的系统调用
- socket
- bind
- listen
- accept
6. 客户端连接
- 新打开一个命令行,进行连接
- nc localhost 9090
- 继续查看系统调用 vim out.2350
accept(3, {sa_family=AF_INET6, sin6_port=htons(44028), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 5
write(1, "client 44028", 12) = 12
write(1, "\n", 1) = 1
stat64("/opt/bak/io/SocketBIO$1.class", {st_mode=S_IFREG|0644, st_size=990, ...}) = 0
openat(AT_FDCWD, "/opt/bak/io/SocketBIO$1.class", O_RDONLY|O_LARGEFILE) = 6
fstat64(6, {st_mode=S_IFREG|0644, st_size=990, ...}) = 0
stat64("/opt/bak/io/SocketBIO$1.class", {st_mode=S_IFREG|0644, st_size=990, ...}) = 0
read(6, "\312\376\272\276\0\0\0.\0B\n\0\20\0\35\t\0\17\0\36\n\0\37\0 \7\0!\7\0\"\n"..., 990) = 990
close(6) = 0
mmap2(NULL, 528384, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0xe8f20000
mprotect(0xe8f21000, 524288, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
clone(child_stack=0xe8fa0424, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0xe8fa0ba8, tls={entry_number=12, base_addr=0xe8fa0b40, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}, child_tidptr=0xe8fa0ba8) = 5526
futex(0x86cfd78, FUTEX_WAIT_PRIVATE, 0, NULL) = 0
futex(0x86a2cb4, FUTEX_WAIT_PRIVATE, 2, NULL) = 0
futex(0x86a2cb4, FUTEX_WAKE_PRIVATE, 1) = 0
sched_setscheduler(5526, SCHED_OTHER, [5]) = -1 EINVAL (Invalid argument)
accept(3,
- 这里可以看到,在未进行客户端连接时阻塞的accept系统调用,已经完成返回值是文件描述符5
- clone 系统调用开辟了一个新的线程,去处理客户端的数据
- 然后继续accept阻塞
7. 客户端发送数据
- 在上面的clone系统调用,返回值是5526
- 我们来查看out.5526 文件
- 最后一行系统调用 recv,传入文件描述符5,就是前面accept返回的文件描述符
recv(5,
- 在nc 窗口下发送数据
$ nc localhost 9090
abc
- 服务端收到数据
$ strace -ff -o out j2sdk1.4.2_19/bin/java SocketBIO
create ServerSocket
client 44028
abc
- 再次查看out.5526 文件
recv(5, "abc\n", 8192, 0) = 4
ioctl(5, FIONREAD, [0]) = 0
write(1, "abc", 3) = 3
write(1, "\n", 1) = 1
recv(5, ### 再次阻塞
8.总结
- 网络编程使用的系统调用
- socket
- bind
- listen
- accept
- recv
- accept 和 recv都是阻塞的
- 由于accept 和 recv都是阻塞的,如果需要处理多个连接, 需要为每个客户端连接创建一个线程,造成大量的线程资源消耗