(可按目录按需阅读,我一般会整理的比较细)
前置知识
Socket
什么是socket?socket字面意思其实就是一个插口或者套接字,包含了源ip地址、源端口、目的ip地址和源端口。
但是socket在那个位置呢 ,在TCP/IP网络的四层体系和OSI七层好像都找不到他的影子,如下图所示, Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。一般由操作系统或者JVM自己实现。java.net中的socket其实就是对底层的抽象调用。有一点需要注意,运行在同一主机上的其他应用程序可能也会通过底层套接字抽象来使用网络,因此会与java socket实例竞争资源,如端口。
工作流程
对于服务器来说,服务器先初始化socket,然后端口绑定(bind),再对端口监听(listen),调用accept阻塞,等待客户端连接请求。对于客户端来说,客户端初始化socket,然后申请连接(connection)。客户端申请连接,服务器接受申请并且回复申请许可(这里要涉及TCP三次握手连接),然后发送数据,最后关闭连接,这是一次交互过程。
角色
服务器
服务器的socket程序有以下几个任务:
- 创建ServerSocket。
- 绑定并监听端口
- 阻塞,等待客户端连接。
- 与客户端连接成功后,进行数据交互
客户端
客户端的socket程序有以下几个任务:
- 创建Socket。
- 连接服务器。
- 与服务器连接成功后,进行数据交互。
代码
服务端代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
final String QUIT = "quit";
final int DEFAULT_PORT = 8000;
ServerSocket serverSocket = null;
BufferedReader reader = null;
BufferedWriter writer = null;
try {
// 绑定监听端口
serverSocket = new ServerSocket(DEFAULT_PORT);
System.out.println("启动服务器,监听服务器本地端口" + DEFAULT_PORT);
while (true) {
// 等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已连接");
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
String msg = null;
while ((msg = reader.readLine()) != null) {
// 读取客户端发送的消息
System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]: " + msg);
// 回复客户发送的消息
writer.write("服务器已收到: " + msg + "\n");
writer.flush();
// 查看客户端是否退出
if (QUIT.equalsIgnoreCase(msg)) {
System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已断开连接");
break;
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
serverSocket.close();
reader.close();
writer.close();
System.out.println("关闭serverSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码
import java.io.*;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
final String QUIT = "quit";
final String DEFAULT_SERVER_HOST = "127.0.0.1";
final int DEFAULT_SERVER_PORT = 8000;
Socket socket = null;
BufferedWriter writer = null;
BufferedReader reader = null;
BufferedReader consoleReader = null;
try {
// 创建socket
socket = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
// 创建IO流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream())
);
// 等待用户输入信息
consoleReader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String input = consoleReader.readLine();
// 发送消息给服务器
writer.write(input + "\n");
writer.flush();
// 读取服务器返回的消息
String msg = reader.readLine();
System.out.println(msg);
// 查看用户是否退出
if (QUIT.equalsIgnoreCase(input)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close(); //关闭之前还会flush一次
socket.close();
reader.close();
consoleReader.close();
System.out.println("关闭socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行展示
客户端
服务端
扩展
服务端大致流程
在创建ServerSocket 实例的时候,他就已经监听了服务器本地的DEFAULT_PORT端口
serverSocket = new ServerSocket(DEFAULT_PORT);
进入构造函数(CTRL进入)
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
// new 实例,里面还有一系列逻辑,里面有工厂可以new 实例
setImpl();
//删除了判断输入参数的代码
try {
//绑定指定端口
bind(new InetSocketAddress(bindAddr, port), backlog);
}
// 删除了出错和异常状态处理的代码
}
bind 函数
public void bind(SocketAddress endpoint, int backlog) throws IOException {
// 删除一系列if语句,用于判断业务的前提环境是否异常
InetSocketAddress epoint = (InetSocketAddress) endpoint;
try {
//删除 SecurityManager 安全管理器的检查,检查该线程是否可以监听该端口
//将此Serversocket绑定到指定的本地 IP 地址和端口号
getImpl().bind(epoint.getAddress(), epoint.getPort());
//实现对该端口的监听,backlog参数是socket上请求的最大挂起连接数
getImpl().listen(backlog);
bound = true; // 标志字段,表示绑定成功
}
//删除了出错和异常状态处理的代码
}
所以,serversocket一经诞生就已经绑定监听了端口,不绑定监听端口说明没有构造完,这也是他天生的职责。
Socket socket = serverSocket.accept();
监听要与此套接字建立的连接并接受它。 该方法阻塞,直到建立连接
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
// 老规矩,if检查
Socket s = new Socket((SocketImpl) null);
//安全管理器的checkAccept方法将使用s.getInetAddress().getHostAddress() s.getPort()和s.getPort()作为其参数调用,以确保允许操作。 这可能会导致 SecurityException。
implAccept(s);
return s;
}
获取向客户端读、写的字符流。(socket的数据肯定是通过运输层协议通信而来的,而网络通信的数据一般为字节流数据,便于网络传输)
InputStreamReader 是字节流通向字符流的桥梁,它将字节流转换为字符流.
OutputStreamWriter是字符流通向字节流的桥梁,它将字符流转换为字节流.
BufferedReader
BufferedWriter
BufferedReader和BufferedWriter 获取到字符流后,可直接缓存,以增加缓冲的方式来提高输入和输出的效率
从read()方法理解,若使用InputStreamReader的read()方法,可以发现存在每2次就会调用一次解码器解码,但若是使用BufferedReader包装InputStreamReader后调用read()方法,可以发现只会调用一次解码器解码,其余时候都是直接从BufferedReader的缓冲区中取字符即可
从read(char cbuf[], int offset, int length)方法理解,若使用InputStreamReader的方法则只会读取leng个字符,但是使用BufferedReader类则会读取读取8192个字符,会尽量提取比当前操作所需的更多字节;
例如文件中有20个字符,我们先通过read(cbuf,0,5)要读取5个字符到数组cbuf中,然后再通过read()方法读取1个字符。那么使用InputStreamReader类的话,则会调用一次解码器解码然后存储5个字符到数组中,然后又调用read()方法调用一次解码器读取2个字符,然后返回1个字符;等于是调用了2次解码器,若使用BufferedReader类的话则是先调用一次解码器读取20个字符到字符缓冲区中,然后复制5个到数组中,在调用read()方法时,则直接从缓冲区中读取字符,等于是调用了一次解码器
因此可以看出BufferedReader类会尽量提取比当前操作所需的更多字节,以应该更多情况下的效率提升,因此在设计到文件字符输入流的时候,我们使用BufferedReader中包装InputStreamReader类即可
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
服务端一行一行的读取读取客户端发送的消息,客户端发送“quit”给服务器时,才表示此客户端要退出
String msg = null;
while ((msg = reader.readLine()) != null) {
// 读取客户端发送的消息
System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]: " + msg);
// 回复客户发送的消息
writer.write("服务器已收到: " + msg + "\n");
writer.flush();
// 查看客户端是否退出
if (QUIT.equalsIgnoreCase(msg)) {
System.out.println("客户端["+socket.getInetAddress()+":"+ socket.getPort() + "]已断开连接");
break;
}
}
最后close()各种资源
finally {
try {
serverSocket.close();
reader.close();
writer.close();
System.out.println("关闭serverSocket");
} catch (IOException e) {
e.printStackTrace();
}
}
客户端大致流程:
socket = new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT);
构造函数
//创建一个流套接字并将其连接到指定主机上的指定端口号。
public Socket(String host, int port) throws UnknownHostException, IOException{
this(host != null ? new InetSocketAddress(host, port) :
new InetSocketAddress(InetAddress.getByName(null), port),
(SocketAddress) null, true);
}
他的跳转太多太细,我这里放一个执行到connect0()的调用栈,后缀一般带0的都是native方法。
connect0()方法会实现到服务器的连接
我们可以看到:连接业务的开始也是写到socket的构造函数里面的。我们new Socket(DEFAULT_SERVER_HOST , DEFAULT_PORT,里面会有专门的方法比如上面的new InetSocketAddress(host, port)来检查host,port的合法性然后生成/127.0.0.1:8000合法的格式(一个SocketAddress实例),总结起来就是一条业务链上,会横插入很多检查性的或者其他的业务代码,然后代码就会跳来跳去。
具体怎么连接传递数据已经被封装好了。
连接好了之后,同理获取向服务器读、写的字符流。
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
写代码获取我们控制台的输入流
consoleReader = new BufferedReader(new InputStreamReader(System.in));
将从控制台获取的数据input,通过writer写入,来与服务器交互信息
while (true) {
String input = consoleReader.readLine();
// 发送消息给服务器
writer.write(input + "\n");
writer.flush();
// 读取服务器返回的消息
String msg = reader.readLine();
System.out.println(msg);
// 查看用户是否退出
if (QUIT.equalsIgnoreCase(input)) {
break;
}
}
最后记得close() 各种资源
finally {
try {
writer.close(); //关闭之前还会flush一次
socket.close();
reader.close();
consoleReader.close();
System.out.println("关闭socket");
} catch (IOException e) {
e.printStackTrace();
}
}
References:
- https://kaven.blog.csdn.net/article/details/104149443
- https://coding.imooc.com/class/381.html
- https://www.cnblogs.com/liusxg/p/3917624.html
- https://blog.csdn.net/jiaomingliang/article/details/45950591
- https://www.cnblogs.com/winterfells/p/8745297.html
- https://blog.csdn.net/ai_bao_zi/article/details/81134801