3、 网络IO:Socket编程BIO和TCP参数
lsof -p
: 查看网络通讯的文件描述符netstat -natp
: 只查看sockettcpdump
: 抓取网络通讯包
测试代码:服务端的
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
开启另一个窗口,监听网络连接的数据包
开启新的窗口启动服务端代码
可以看到开启一个9090端口,然后阻塞住了,等待输入
然后切换窗口查看socket
这个服务端开启了一个端口是监听状态
然后再开一个窗口启动客户端
可以看到客户端启动了,等待用户输入
这个时候别急着操作,切换到监听网络连接的数据包,可以看到三次握手已经建立
建立三次握手以后,再来查看一下socket,发现9090端口建立起了连接,有了相应的四元组。
但是没有分配给所对应的进程使用,Recv接收数据为0,Send发送数据也为0
然后客户端发送了1111,四个1,回车之后发送了过去
查看连接数据包,可以看到有交互的
再查看socket连接,可以看到还是没有显示分配给哪个进程使用的,但是内核里已经接受到了4个字节
然后查看这个7932(连接的监听状态端口),虽然上面内核里已经建立连接,得到了四元组,并且接受到了客户端的数据,虽然没有分配给对于的进程使用,但是程序还是监听状态的
接下来让连接分给对应的程序:
服务端点击回车确认,服务端就结束阻塞,代码就执行到了Socket client = server.accept();
,调用了.accept()方法后,分配了一个进程,并且接受了4个字节,也读到了接受的字符,四个1
这个时候再看socket连接,发现已经出现分配给了7932端口
然后查看这个7932的网络连接的文件描述符已经得到了
TCP是面向连接的、可靠的传输协议,要经过三次握手,然后内核开辟资源(客户端执行server.accept()后)
当内核分配了四元组以后,程序如果想要使用这个连接,会有一个网络通信的文件描述符,如图中FD3,如果有个程序使用另外一个连接,就会有另一个文件描述符,当然可以使用FD3,因为是不同的连接进程,相互是隔离的。
至于端口号被占用的问题,比如图中服务端0.0.0.0: 0端口,和客户端的0.0.0.0: 80端口进行了listen连接,如果这个时候客户又要进行另一个和服务端的连接,但是有两个80端口,四元组出现了两个一样的,没有了唯一性,所以第二个连接不知道分给哪个80端口进行连接了,就冲突了,所以报错。
程序应用要使用连接时候产生的这个文件描述符,是个抽象的,指向了内核里的socket连接,每个连接有绑定个buffer,缓冲区可以读写两个方向。
如果有另外一个客户端连接过来服务端应用,那就是另外一个连接了。
客户端也是一个应用,所以也是有内核的,一样也有三次握手,和服务端是一样的,客户端也有自己连接的文件描述符。
BACK_LOG 参数
先把服务端代码跑起来,BACK_LOG 参数设置是2
查看socket,9090端口是监听状态
启动客户端
和上面一样,发现了内核级的三次握手,服务端程序是阻塞的
再启动一个客户端
然后查看socket,有2个连接了,都是在内核级完成了连接,还没有具体的分配给某个进程
这个时候如果再启动新的客户端进行连接,是不行的,服务端只接受2个
MTU 和 MSS
启动服务端
开另一个窗口连接服务端端口
然后不停的一直发送数据
然后查看socket,发现这个连接一直是1920,涨不上去了
客户端最后再发送一些1
然后查看socket其实还是1920,这个时候在服务端程序回车,不再阻塞
然后就会读到一些数据,但是发现最后的那些个1111没有读到,说明只接收到了前面的,后面超过1920大小的数据都给丢失了
发送数据包可以大于缓冲区的配置调优
启动服务端
修改客户端配置,在下面的代码中虽然是写的一个一个字节发送的,但这个配置可以调优成发送一个数据包直接给服务端
然后编译,运行
然后服务端直接回车,不再阻塞
然后客户端发送数据
然后查看服务端接受数据
数据明显超过了客户端设置的20缓冲区大小,说明一个数据包是可以超过缓冲区的大小
接下来,重新测试,把客户端的配置setTcpNoDelay()为true,表示缓冲区满了就发送,不用攒着了
然后重新编译,启动,发送数据给服务端
服务端可以看到效果,确实发现客户端发送数据没有攒着,有数据就立马发送了