Socket
两台计算机使用Socket套接字进行 TCP 连接数据传输时过程如下:
- 服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
- 服务器调用 ServerSocket 类的 accept() 方法,该方法一直会等待,直到客户端连接到服务器上给定的端口。
- 服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
- Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
- 在服务器端,accept() 方法返回服务器上一个新的 Socket 引用,该 Socket 连接到客户端的 Socket。
以上是使用 Socket 套接字实现TCP连接数据传输过程,当然使用 Socket 套接字也可以实现 UDP 数据传输,使用 UDP 就比较简单了,客户端和服务器端不需要建立连接,直接通过数据报进行数据交互即可,缺点就是无状态。
使用 Socket 套接字编程需要用到的工具类。
1. InteAddress
InteAddress 全称 Internet Protocol(IP)Address
。
InetAddress的使用
序号 | 方法描述 | |
---|---|---|
1 | static InetAddress getByAddress(byte[] addr) | 在给定原始 IP 地址的情况下,返回 InetAddress 对象。 |
2 | static InetAddress getByAddress(String host, byte[] addr) | 根据提供的主机名和 IP 地址创建 InetAddress。 |
3 | static InetAddress getByName(String host) | 在给定主机名的情况下确定主机的 IP 地址。 |
4 | String getHostAddress() | 返回 IP 地址字符串(以文本表现形式)。 |
5 | String getHostName() | 获取此 IP 地址的主机名。 |
6 | static InetAddress getLocalHost() | 返回本地主机。 |
7 | String toString() | 将此 IP 地址转换为 String。 |
2. URL
URL 中所指明的协议可以为:http、https、file等等。例如:file:///D:/test.txt
,file:// -》协议;/D:/test.txt 资源路径。根据协议的不同都会有默认端口,例如http默认端口是80,https默认端口是443.
URL 构造方法。
序号 | 方法描述 | |
---|---|---|
1 | public URL(String protocol, String host, int port, String file) throws MalformedURLException | 通过给定的参数(协议、主机名、端口号、文件名)创建URL。 |
2 | public URL(String protocol, String host, String file) throws MalformedURLException | 使用指定的协议、主机名、文件名创建URL,端口使用协议的默认端口。 |
3 | public URL(String url) throws MalformedURLException | 通过给定的URL字符串创建URL |
4 | public URL(URL context, String url) throws MalformedURLException | 使用基地址和相对URL创建 |
URL 对象方法的使用。
序号 | 方法 | 描述 |
---|---|---|
1 | public String getPath() | 返回URL路径部分。 |
4 | public int getPort() | 返回URL端口部分 |
5 | public int getDefaultPort() | 返回协议的默认端口号。 |
6 | public String getProtocol() | 返回URL的协议 |
7 | public String getHost() | 返回URL的主机 |
8 | public String getFile() | 返回URL文件名部分 |
10 | public URLConnection openConnection() throws IOException | 打开一个URL连接,并运行客户端访问资源。 |
案例
public static void main(String[] args) throws IOException {
URL resource = new URL("file:///D:/a.txt");
URLConnection urlConnection = resource.openConnection();
InputStream inputStream = urlConnection.getInputStream();
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) != -1){
System.out.println(new String(buf,0,len));
}
}
代码测试
服务端实现
public class SocketServer {
public static void main(String[] args) throws IOException {
// 创建 Socket 服务端对象
ServerSocket serverSocket = new ServerSocket();
// 绑定 IP+端口号
serverSocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(),8888));
while(true){
// 请求客户端的连接,请求成功会返回客户端的 Socket 对象,失败会一直阻塞
Socket accept = serverSocket.accept();
System.out.println("已和客户端建立连接");
// 通过Socket中的inputstream去读取客户端发送来的数据报
InputStream inputStream = accept.getInputStream();
byte[] readBytes = new byte[1024];
int read = -1;
if (read != -1) {
System.out.println(new String(msg, 0, read));
}
// while ((read = inputStream.read(readBytes))!=-1) {
// System.out.println(new String(readBytes,0,read));
// }
}
}
}
客户端实现
public class SocketClient {
public static void main(String[] args) throws IOException {
// 创建 Socket 对象 指代客户端
Socket socket = new Socket();
// 与服务端建立 TCP 连接
socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(),8888));
// 通过 socket 中的输出流进行发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write("I'm OK!".getBytes());
outputStream.close();
socket.close();
}
}
运行结果
BIO(同步并阻塞)
测试案例的分析
上面的测试代码中是存在阻塞问题的,一处是请求客户端连接的地方,一处是使用 InputStream
去拿到客户端的内容(即 read
的时候)。
上面测试代码会出现以下所述问题:服务端在处理完第一个客户端的所有时间之前,无法为其他客户端提供服务。
上面那个服务,如果我开启两个cmd窗口,然后通过 telnet 127.0.0.1 8888
用来指示两个客户端来测试服务,则会出现同时申请建立连接会出现下面情况:即显示了一次“已和客户端建立连接”。这说明后面开的客户端建立的连接被阻塞了。(如果是想开启 telnet
服务,需在windows下的控制面板-》程序-》启动或关闭Windows功能-》搜telnet 打开telnet 客户端即可。)
只有当一个客户端发送完数据被接受完后才会和第二个客户端建立连接:
解决方法:
阻塞的原因很简单就可以看出,就是是主线程去处理的读取客户端数据操作时,被阻塞了,导致主线程无法去处理监听到的其他客户端,那现在将处理读取客户端数据的业务新建一个线程去完成,就不会出现上面的问题了:阻塞客户端的连接,阻塞客户端发送数据。
while (true) {
Socket accept = serverSocket.accept();
System.out.println("已和客户端建立连接");
new Thread(() -> {
try {
byte[] msg = new byte[1024];
InputStream inputStream = accept.getInputStream();
int read = -1;
read = inputStream.read(msg);
if (read != -1) {
System.out.println(new String(msg, 0, read));
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
先在开启两个窗口去扮演客户端身份访问服务端,即会出现下面现象:两个都成功建立了连接。
发送消息和接受消息也都是🆗的。
但是现在也存在一个问题:就是当我客户端没有向服务端发送数据的时候,那服务端对于处理接受数据的业务操作对于的线程就不会结束,就会一直阻塞且存在于内存中,那当我们有大量这样的连接的时候,就会有好多好多线程,最后会 IMEMORYERROR。
通过案例分析引出BIO概念
Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
缺点:
- IO 代码里 read 操作是阻塞操作,如果连接不做数据读写操作会造成线程阻塞,浪费资源。
- 如果线程很多,会导致服务器线程太多,压力太大。
总结:如果说客户端只是建立一个连接而已,不进行任何读写,这个线程可以说是没有干任何事情的。