3、 网络IO:Socket编程BIO和TCP参数

  • lsof -p: 查看网络通讯的文件描述符
  • netstat -natp: 只查看socket
  • tcpdump: 抓取网络通讯包

测试代码:服务端的

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO  多线程的方式
 */
public class SocketIOPropertites {


    //server socket listen property:
    private static final int RECEIVE_BUFFER = 10;
    private static final int SO_TIMEOUT = 0;
    private static final boolean REUSE_ADDR = false;
    private static final int BACK_LOG = 2;	// 设置处理客户端请求的线程数量
	
    //client socket listen property on server endpoint:
    private static final boolean CLI_KEEPALIVE = false;		// 是否是长连接
    private static final boolean CLI_OOB = false;	// 连接断开时是否试探一下连接
    private static final int CLI_REC_BUF = 20;	// Recv,接受到的数据
    private static final boolean CLI_REUSE_ADDR = false;	// 是否重定地址
    private static final int CLI_SEND_BUF = 20;
    private static final boolean CLI_LINGER = true;		// 断开连接速度
    private static final int CLI_LINGER_N = 0;
    private static final int CLI_TIMEOUT = 0;	// 读取超时设置
    private static final boolean CLI_NO_DELAY = false;		// 是否开启缓存
/*

    StandardSocketOptions.TCP_NODELAY
    StandardSocketOptions.SO_KEEPALIVE
    StandardSocketOptions.SO_LINGER
    StandardSocketOptions.SO_RCVBUF
    StandardSocketOptions.SO_SNDBUF
    StandardSocketOptions.SO_REUSEADDR

 */


    public static void main(String[] args) {

        ServerSocket server = null;
        try {
            server = new ServerSocket();
            server.bind(new InetSocketAddress(9090), BACK_LOG);
            server.setReceiveBufferSize(RECEIVE_BUFFER);
            server.setReuseAddress(REUSE_ADDR);
            server.setSoTimeout(SO_TIMEOUT);

        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("server up use 9090!");
        try {
            while (true) {

                // System.in.read();  //分水岭:ServerSocket连接后,必须回车确认,不然不会往下走

				// 开始采用处理客户端请求,只有执行这一行代码,内核才会分配建立3次握手后的网络连接的文件描述符,也就是这个返回的Socket,java里面包装成了这个对象
                Socket client = server.accept();  //阻塞的,没有 -1  一直卡着不动  accept(4,
                System.out.println("client port: " + client.getPort());

                client.setKeepAlive(CLI_KEEPALIVE);
                client.setOOBInline(CLI_OOB);
                client.setReceiveBufferSize(CLI_REC_BUF);
                client.setReuseAddress(CLI_REUSE_ADDR);
                client.setSendBufferSize(CLI_SEND_BUF);
                client.setSoLinger(CLI_LINGER, CLI_LINGER_N);
                client.setSoTimeout(CLI_TIMEOUT);
                client.setTcpNoDelay(CLI_NO_DELAY);

                //client.read   //阻塞   没有  -1 0
                new Thread(
                        () -> {
                            try {
								// 拿到客户端输入流
                                InputStream in = client.getInputStream();
                                BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                                char[] data = new char[1024];
                                while (true) {

                                    int num = reader.read(data);

									// 打印读取到客户端的数据
                                    if (num > 0) {
                                        System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
                                    } else if (num == 0) {
                                        System.out.println("client readed nothing!");
                                        continue;
                                    } else {
                                        System.out.println("client readed -1...");	// -1表示客户端断开了
                                        System.in.read();
                                        client.close();
                                        break;
                                    }
                                }

                            } catch (IOException e) {
                                e.printStackTrace();
                            }

                        }
                ).start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

客户端的测试代码:

import java.io.*;
import java.net.Socket;

public class SocketClient {

    public static void main(String[] args) {

        try {
			// new Socket连接一台机器
            Socket client = new Socket("192.168.150.11",9090);

            client.setSendBufferSize(20);	// 设置发送缓冲区
            client.setTcpNoDelay(true);
            OutputStream out = client.getOutputStream();	// 得到输出流

            InputStream in = System.in;
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));

            while(true){
                String line = reader.readLine();
                if(line != null ){
                    byte[] bb = line.getBytes();
					// 把手动输入的数据输出到字节数组,但是注意没有flush
                    for (byte b : bb) {
                        out.write(b);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

准备测试
先使用netstat -natp查看现有的socket
image

开启另一个窗口,监听网络连接的数据包
image

开启新的窗口启动服务端代码
image
可以看到开启一个9090端口,然后阻塞住了,等待输入

然后切换窗口查看socket
image
这个服务端开启了一个端口是监听状态

然后再开一个窗口启动客户端
image
可以看到客户端启动了,等待用户输入

这个时候别急着操作,切换到监听网络连接的数据包,可以看到三次握手已经建立
image

建立三次握手以后,再来查看一下socket,发现9090端口建立起了连接,有了相应的四元组。
但是没有分配给所对应的进程使用,Recv接收数据为0,Send发送数据也为0
image

然后客户端发送了1111,四个1,回车之后发送了过去
image

查看连接数据包,可以看到有交互的
image

再查看socket连接,可以看到还是没有显示分配给哪个进程使用的,但是内核里已经接受到了4个字节
image

然后查看这个7932(连接的监听状态端口),虽然上面内核里已经建立连接,得到了四元组,并且接受到了客户端的数据,虽然没有分配给对于的进程使用,但是程序还是监听状态的
image

接下来让连接分给对应的程序:
服务端点击回车确认,服务端就结束阻塞,代码就执行到了Socket client = server.accept();,调用了.accept()方法后,分配了一个进程,并且接受了4个字节,也读到了接受的字符,四个1
image

这个时候再看socket连接,发现已经出现分配给了7932端口
image

然后查看这个7932的网络连接的文件描述符已经得到了
image

TCP是面向连接的、可靠的传输协议,要经过三次握手,然后内核开辟资源(客户端执行server.accept()后)
image

image
当内核分配了四元组以后,程序如果想要使用这个连接,会有一个网络通信的文件描述符,如图中FD3,如果有个程序使用另外一个连接,就会有另一个文件描述符,当然可以使用FD3,因为是不同的连接进程,相互是隔离的。

至于端口号被占用的问题,比如图中服务端0.0.0.0: 0端口,和客户端的0.0.0.0: 80端口进行了listen连接,如果这个时候客户又要进行另一个和服务端的连接,但是有两个80端口,四元组出现了两个一样的,没有了唯一性,所以第二个连接不知道分给哪个80端口进行连接了,就冲突了,所以报错。

程序应用要使用连接时候产生的这个文件描述符,是个抽象的,指向了内核里的socket连接,每个连接有绑定个buffer,缓冲区可以读写两个方向。
如果有另外一个客户端连接过来服务端应用,那就是另外一个连接了。
客户端也是一个应用,所以也是有内核的,一样也有三次握手,和服务端是一样的,客户端也有自己连接的文件描述符。
image

BACK_LOG 参数

先把服务端代码跑起来,BACK_LOG 参数设置是2
image

查看socket,9090端口是监听状态
image

启动客户端
image

和上面一样,发现了内核级的三次握手,服务端程序是阻塞的
再启动一个客户端
image

然后查看socket,有2个连接了,都是在内核级完成了连接,还没有具体的分配给某个进程
image

这个时候如果再启动新的客户端进行连接,是不行的,服务端只接受2个

MTU 和 MSS

启动服务端
image

开另一个窗口连接服务端端口
image

然后不停的一直发送数据
image

然后查看socket,发现这个连接一直是1920,涨不上去了
image

客户端最后再发送一些1
image

然后查看socket其实还是1920,这个时候在服务端程序回车,不再阻塞
然后就会读到一些数据,但是发现最后的那些个1111没有读到,说明只接收到了前面的,后面超过1920大小的数据都给丢失了
image

发送数据包可以大于缓冲区的配置调优

启动服务端
image

修改客户端配置,在下面的代码中虽然是写的一个一个字节发送的,但这个配置可以调优成发送一个数据包直接给服务端
image

然后编译,运行
image

然后服务端直接回车,不再阻塞
image

然后客户端发送数据
image

然后查看服务端接受数据
image
数据明显超过了客户端设置的20缓冲区大小,说明一个数据包是可以超过缓冲区的大小

接下来,重新测试,把客户端的配置setTcpNoDelay()为true,表示缓冲区满了就发送,不用攒着了
image

然后重新编译,启动,发送数据给服务端
image

服务端可以看到效果,确实发现客户端发送数据没有攒着,有数据就立马发送了
image

posted @ 2022-11-25 15:51  aBiu--  阅读(63)  评论(0编辑  收藏  举报