Socket
服务端代码
package com
.bjmashibing
.system
.io
;
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
;
public class SocketIOPropertites {
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;
private static final boolean CLI_KEEPALIVE
= false;
private static final boolean CLI_OOB
= false;
private static final int CLI_REC_BUF
= 20;
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;
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) {
Socket client
= server
.accept();
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
);
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...");
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();
}
}
}
}
客户端代码
package com
.bjmashibing
.system
.io
;
import java
.io
.*
;
import java
.net
.Socket
;
public class SocketClient {
public static void main(String
[] args
) {
try {
Socket client
= new Socket("192.168.150.11",9090);
client
.setSendBufferSize(20);
client
.setTcpNoDelay(true);
client
.setOOBInLine(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();
for (byte b
: bb
) {
out
.write(b
);
}
}
}
} catch (IOException e
) {
e
.printStackTrace();
}
}
}
踪建立连接的过程 启动服务端 开启服务端后,出现了一个对于 9090 的 listen 状态。 TCP 三次握手是走 listen 的,建立连接之后,后面走文件描述符,那就是另外一个环节了,我们后面再讲。 使用jps得到服务端的进程id号:7932 使用lsof -p 7932查看7932端口的文件描述符的分配情况。 启动客户端 客户端启动,进入代码的阻塞等待用户输入逻辑 在服务端抓到了三次握手的包 在服务端看到建立了连接,虽然连接还未被使用。 在客户端进行用户输入之后(服务端也有的阻塞的逻辑,需要回车才能接收client的数据) 继续查看服务端抓包监听 查看服务端的连接状态:双方开辟了资源。即便你程序不要我,我也在内核里有资源用来接收或者等待一类的 服务端输入回车之后 接受到了客户端发过来的数据 刚才的socket连接已经被分配给7932了 lsof 得到了新的文件描述符 6 结论 TCP:面向连接的,可靠的传输协议 Socket:是一个四元组。ip:port ip:port四元组的任何一个元的不同,都可以区分不同的连接。
面试题 1:服务端80端口接收客户端连接之后,是否需要为客户端的连接分配一个随机端口号? 答:不需要。
面试题 2:现在,有一个客户端,有一个服务端, 客户端的ip地址是AIP,程序使用端口号CPORT想要建立连接。 服务端的IP地址是XIP,端口号是XPORT。 现在假设某一个客户端A开了很多连接占满了自己的65535个端口号,那客户端A是否还能与另一个服务端建立建立连接? 答:可以,因为只要能保证四元组唯一即可 注:一台服务器是可以与超过65535个客户端保持长连接的,调优到超过百万连接都没问题,只要四元组唯一就可以了。客户端来了之后,服务端是不需要单独给它开辟一个端口号的。 下面这个图可以说明,无论再多的连接,服务端始终是使用的同一个ip:端口 那么,我们常见的报错“端口号被占用”是什么原因? 我们常见的报错“端口号被占用”实际上是在启动SocketSocket的时候,而不是Socket,两者不是一个概念。如果两个服务使用了相同的端口号,这时如果来了一个数据包,内核无法区分是哪一个服务在LISTEN,不知道要发给哪一个服务了,如下图例子
BIO
服务端代码:
import java
.io
.IOException
;
import java
.net
.ServerSocket
;
import java
.net
.Socket
;
import java
.time
.LocalDateTime
;
public class ServerSingle {
public static void main(String
[] args
) {
byte[] buffer
= new byte[1024];
try {
ServerSocket serverSocket
= new ServerSocket(7001);
System
.out
.println(LocalDateTime
.now() + " " + "服务器已启动并监听7001端口");
while (true) {
System
.out
.println(LocalDateTime
.now() + " " + "服务器正在等待连接...");
Socket socket
= serverSocket
.accept();
System
.out
.println(LocalDateTime
.now() + " " + "客户端端口 " + socket
.getPort() + "请求: " + "服务器已接收到连接请求..." + socket
.getInetAddress() + " " + socket
.getPort());
System
.out
.println(LocalDateTime
.now() + " " + "客户端端口 " + socket
.getPort() + "请求: " + "服务器正在等待数据...");
socket
.getInputStream().read(buffer
);
System
.out
.println(LocalDateTime
.now() + " " + "客户端端口 " + socket
.getPort() + "请求: " + "服务器已经接收到数据");
String content
= new String(buffer
);
System
.out
.println(LocalDateTime
.now() + " " + "客户端端口 " + socket
.getPort() + "请求: " + "接收到的数据:" + content
);
}
} catch (IOException e
) {
e
.printStackTrace();
}
}
}
客户端代码:
import java
.io
.IOException
;
import java
.net
.Socket
;
import java
.util
.Scanner
;
public class Consumer {
public static void main(String
[] args
) {
try {
Socket socket
= new Socket("127.0.0.1", 7001);
String message
= null
;
Scanner sc
= new Scanner(System
.in
);
message
= sc
.next();
socket
.getOutputStream().write(message
.getBytes());
socket
.close();
sc
.close();
} catch (IOException e
) {
e
.printStackTrace();
}
}
}
启动服务端
2020-03-16T12
:01:20.766 服务器已启动并监听
7001端口
2020-03-16T12
:01:20.766 服务器正在等待连接…
通过CMD客户端启动客户端。SocketIO_01\target\classes>java mybio1.Consumer
2020-03-16T12
:01:20.766 服务器已启动并监听
7001端口
2020-03-16T12
:01:20.766 服务器正在等待连接…
2020-03-16T12
:01:53.862 客户端端口
55104请求: 服务器已接收到连接请求…
/127.0.0.1 55104
2020-03-16T12
:01:53.862 客户端端口
55104请求: 服务器正在等待数据…
客户端输入数据:12312312323214
2020-03-16T12
:01:20.766 服务器已启动并监听
7001端口
2020-03-16T12
:01:20.766 服务器正在等待连接…
2020-03-16T12
:01:53.862 客户端端口
55104请求: 服务器已接收到连接请求…
/127.0.0.1 55104
2020-03-16T12
:01:53.862 客户端端口
55104请求: 服务器正在等待数据…
2020-03-16T12
:02:35.160 客户端端口
55104请求: 服务器已经接收到数据
2020-03-16T12
:02:35.160 客户端端口
55104请求: 接收到的数据
:12312312323214
2020-03-16T12
:02:35.160 服务器正在等待连接…
结论 从上文的运行结果中我们可以看到,服务器端在启动后,首先需要等待客户端的连接请求(第一次阻塞),如果没有客户端连接,服务端将一直阻塞等待,然后当客户端连接后,服务器会等待客户端发送数据(第二次阻塞),如果客户端没有发送数据,那么服务端将会一直阻塞等待客户端发送数据。服务端从启动到收到客户端数据的这个过程,将会有两次阻塞的过程。这就是BIO的非常重要的一个特点,BIO会产生两次阻塞,第一次在等待连接时阻塞,第二次在等待数据时阻塞。
在单线程条件下BIO的弱点 在上文中,我们实现了一个简易的服务器,这个简易的服务器是以单线程运行的,其实我们不难看出,当我们的服务器接收到一个连接后,并且没有接收到客户端发送的数据时,是会阻塞在read()方法中的,那么此时如果再来一个客户端的请求,服务端是无法进行响应的。换言之,在不考虑多线程的情况下,BIO是无法处理多个客户端请求的。
BIO如何处理并发 在刚才的服务器实现中,我们实现的是单线程版的BIO服务器,不难看出,单线程版的BIO并不能处理多个客户端的请求,那么如何能使BIO处理多个客户端请求呢。其实不难想到,我们只需要在每一个连接请求到来时,创建一个线程去执行这个连接请求,就可以在BIO中处理多个客户端请求了,这也就是为什么BIO的其中一条概念是服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。
优化 为了让服务端能够并发处理多个客户端消息,在服务端增加子进程。
最终模型