博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
追踪、分析Java网络编程底层系统调用
阅读量:2491 次
发布时间:2019-05-11

本文共 5950 字,大约阅读时间需要 19 分钟。

本文将会在Linux环境中让服务端与客户端通过BIO的网络模型进行通信,并且演示整个通信细节。

命令介绍

在分之前会先介绍几个命令,并说明在本次分析中的作用。

1、lsof -p pid

我们用losf命令主要是为了查看系统为某个进程分配的文件描述符的信息。

-p:列出指定进程号所打开的文件。

2、netstat -natp

显示协议统计信息和当前 TCP/IP网络连接。

-n:以数字形式显示地址和端口号。

-a:显示所有连接和监听端口。
-t:显示TCP传输协议的连线状况。
-p:显示正在使用Socket的程序识别码和程序名称。

3、strace -ff -o

常用来跟踪进程执行时的系统调用和所接收的信号。,包括参数,返回值,执行消耗的时间。

-ff:如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。

-o:后面跟filename,将strace的输出写入文件filename中。
-p:跟踪指定的进程pid。

4、tcpdump -S -nn -i eth0 port 9090

根据使用者的定义对网络上的数据包进行截获的包分析工具。

-S:用绝对而非相对数值列出TCP关联数。

-n:不把主机的网络地址转换成名字。
-i:使用指定的网络截面送出数据包。

TCP连接常见状态

演示中会看到如下状态中的部分状态信息,可先行了解。

LISTEN

侦听来自远方的TCP端口的连接请求

SYN-SENT

在发送连接请求后等待匹配的连接请求

SYN-RECEIVED

在收到和发送一个连接请求后等待对方对连接请求的确认

ESTABLISHED

代表一个打开的连接

FIN-WAIT-1

等待远程TCP连接中断请求,或先前的连接中断请求的确认

FIN-WAIT-2

从远程TCP等待连接中断请求

CLOSE-WAIT

等待从本地用户发来的连接中断请求

CLOSING

等待远程TCP对连接中断的确认

LAST-ACK

等待原来的发向远程TCP的连接中断请求的确认

TIME-WAIT

等待足够的时间以确保远程TCP接收到连接中断请求的确认

CLOSED

没有任何连接状态

代码部分

服务端代码

服务端首先通过ServerSocket(9090),创建一个socket,之后循环接收客户端连接,但为了演示效果,每次接收新的连接之前,先通过System.in.read()方法阻塞住,当按下任意键时,来到serverSocket.accept()方法,监听客户端的请求到来。

当接收到客户端请求后,会立刻开启一个新的线程,并将请求交由新的线程去处理,主线程则继续循环等待新的客户端请求到来。

新创建出来的线程则通过bufferedReader.readLine()方法,接收客户端发送的数据并在打印输出之后,循环等待数据的到来。

整个过程会看到accept、readLine两个方法都会产生阻塞(当然System.in.read()的read方法也会阻塞,但只是为了效果演示,真正处理时也不需要调用这个方法。)

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 ServerSocketBIO {
public static void main(String[] args) {
ServerSocket serverSocket = null; try {
serverSocket = new ServerSocket(9090); System.out.println("--server start--"); while (true) {
//在accept之前,先阻塞住直到按下任意键,方便分析调用流程 System.in.read(); Socket client = serverSocket.accept(); System.out.println("---accept client ---"); new Thread(() -> {
InputStream inputStream; try {
inputStream = client.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); while (true) {
System.out.println("*wait client send data...*"); String readLine = bufferedReader.readLine(); if (readLine.equals("quit")) {
System.out.println("---client down...---"); bufferedReader.close(); inputStream.close(); client.close(); break; } else {
System.out.println("recv client data:" + readLine); } } } catch (IOException e) {
e.printStackTrace(); } }).start(); } } catch (IOException e) {
e.printStackTrace(); } finally {
try {
serverSocket.close(); } catch (IOException e) {
e.printStackTrace(); } } }}

客户端代码

客户端代码就非常简单,请求连接服务端后,将数据通过socket发送出去即可。

import java.io.*;import java.net.Socket;public class SocketCli {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("10.0.0.101", 9090); System.out.println("client start..."); OutputStream outputStream = socket.getOutputStream(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream)); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while (true) {
String readLine = reader.readLine(); if (readLine.equals("quit")) {
bufferedWriter.write(readLine); bufferedWriter.newLine(); bufferedWriter.flush(); System.exit(-1); } else {
bufferedWriter.write(readLine); bufferedWriter.newLine(); bufferedWriter.flush(); } } }}

追踪分析

开启tcpdump抓取数据包

在这里插入图片描述

启动服务端

在这里插入图片描述

查看strace输出的out文件,分析如下:

1、服务端创建ServerSocket时,会通过调用socket函数完成,并得到一个FD=5。

2、通过bind函数,将5绑定到9090端口上。

3、通过listen函数,开启监听。

4、通过write打印输出。

5、最终阻塞在read函数中,等待任意键输入(阻塞在这行代码System.in.read())。

在这里插入图片描述

查看为服务端分配的文件描述符,FD5对应一个TCP连接,并且状态为LISTEN。

在这里插入图片描述


启动一个客户端。

在这里插入图片描述

注意此时服务端并没有通过accept接收客户端的请求,但是在TCP层面双方已经完成了三次握手(意味着已经可以进行数据传输了)。

在这里插入图片描述

服务端建立了连接,因为没有accept,所以也没分配PID,但是状态已经为ESTABLISHED了。

在这里插入图片描述

再去查询客户端,客户端也建立连接,并分配了PID,状态为ESTABLISHED。

在这里插入图片描述

服务端键入任意键,表示接收了客户端请求。(意味着调用了serverSocket.accept())

在这里插入图片描述

1、通过阻塞式函数poll,等待FD=5的文件描述符就绪,如果没有客户端请求到来,则会一直阻塞在这个方法上(也就是serverSocket.accept(),这也是一个阻塞点)。

2、调用accept函数,创建一个与10.0.0.101建立连接的socket,并返回一个引用这个socket新的FD=6。

在这里插入图片描述

3、accept之后,代码中是直接创建了一个新的线程处理,所以当java中调用new Thread时,实际上在linux中通过clone这个函数完成的,并返回了新的线程pid,5667。

4、之后继续阻塞在read函数(也就是回到了代码中的System.in.read()),等待任意键输入。

在这里插入图片描述

此时再来看服务端打开的文件信息时,多了一条FD=6的TCP信息。

在这里插入图片描述

之前未分配PID,现在也已经完成了分配。

在这里插入图片描述

再来查看strace跟踪到的5667这个新创建出来的线程,阻塞在了recvfrom函数中,也是bufferedReader.readLine()这行代码,等待socket中的消息到来。

在这里插入图片描述


现在让客户端发送一点数据

在这里插入图片描述
服务端可以正常接收
在这里插入图片描述

5667线程接收到输出后,继续等待新的数据到来。

在这里插入图片描述


现在让我们再启动一个客户端,服务端正常接收连接。

在这里插入图片描述

服务端依然通过clone函数,创建一个新的线程, 并且pid为5674。

在这里插入图片描述

服务端进程中又多了一条FD=7的TCP信息。

在这里插入图片描述

正常建立了连接。

在这里插入图片描述

新的线程同样等待数据到来。

在这里插入图片描述


最后我们让一个客户端下线,观察netstat,此时下线的客户端与服务端的连接状态为TIME_WAIT。

在这里插入图片描述

分配的FD=6也没了。

在这里插入图片描述


最后再证实一下accept方法会阻塞。

重启服务端,直接按下任意键,此时并没有任何客户端的连接请求。

在这里插入图片描述

可以看到请求阻塞在了poll函数中,对于的就是serverSocket.accept()代码。

在这里插入图片描述


总结

当服务端创建一个socket并绑定到9090端口上时,系统会为服务端进程分配一个FD并专门用来监听客户端的请求,当有客户端连接时,即使服务端没有accept,也会完成三次握手并建立连接,只不过对于服务端来说此时建立的连接并没有分配到某个具体的PID上,一旦服务端调用accept接收客户端的连接后,就会创建一个新的FD,专门用来处理服务端与客户端数据的交互。

通过演示我们也能看到在传统BIO模式下,服务端的accept和read都会导致线程阻塞,所以我们让主线程专门用来监听客户端的请求,把监听到的请求全部交给一个新的线程去处理,这样实现了一个服务端能同时接收多个客户端的需求,但是你始终不能无限的创建线程,它始终会有瓶颈,所以之后也就出现了NIO,多路复用等IO模型,在这些模型下可以完成一个线程同时处理多个客户端的请求。

至此也就完成了最简单的BIO模式下的系统调用分析,大家可以参考文章,自己进行实验。

感谢

最后感谢各位老铁:点赞、收藏、评论

转载地址:http://wqlrb.baihongyu.com/

你可能感兴趣的文章
Yii2的一些问题
查看>>
LeetCode OJ - Populating Next Right Pointers in Each Node II
查看>>
C++ wifstream读取日文方法(中文适用)
查看>>
B-树
查看>>
php计算上个月是几月份
查看>>
浅谈 trie树 及其实现
查看>>
60款很酷的 jQuery 幻灯片演示和下载
查看>>
JavaScript数组内置排序函数
查看>>
hdu 3549 Flow Problem(最大流模板题)
查看>>
nyoj-20-吝啬的国度(深搜)
查看>>
Vue.js(2.x)之Class 与 Style 绑定
查看>>
属性“dataProvider”有多个初始值设定项。(注意:“dataProvider”是“mx.charts.BarChart”的默认属性)。...
查看>>
C3P0在多线程下的maxPoolSize配置
查看>>
宽客的人&&事件映射
查看>>
linux(fedora) 下dvwa 建筑环境
查看>>
Oracle 跨库 查询 复制表数据 分布式查询
查看>>
Python笔记(十五)_异常处理
查看>>
C\C++ 获取当前路径
查看>>
Pandas CookBook -- 04选取数据子集
查看>>
正则表达式语法
查看>>