(可按目录按需阅读,我一般会整理的比较细)
前置知识
java IO
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 = new Socket(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
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();
socket.close();
reader.close();
consoleReader.close();
System.out.println("关闭socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行展示
客户端
服务端
CMD查看
开启Server端之后,在Windows cmd 终端里面输入命令,会发现8000端口处于LISTENING状态,先前没开启Server代码运行是没有的
netstat -ano|findstr "8000"
netstat 用于显示套接字内容 , -ano 是可选选项 a不仅显示正在通信的套接字,还显示包括尚未开始通信等状态的所有套接字 n 显示 IP 地址和端口号 o 显示套接字的程序 PID
第一列表示通信协议,这里是TCP 第二列表示,运行netstat命令的主机ip和port,这里也就是服务器的IP和port,0.0.0.0表示还没有绑定IP地址 第三列表示,通信对象的IP和port,0.0.0.0:0表示还没连接到对象,所以IP和port都不知道 第四列表示,LISTENING表示等待对方连接 最后一列,PID进程号 图中的每一行都相当于一个套接字,每一列也被称为一个元组,所以一个套接字就是五元组(协议、本地地址、外部地址、状态、PID),有的时候也被叫做四元组,四元组不包括协议。
开启客户端建立连接通信之后 可以看到开了两个进程,因为我们客户端和服务端都是在一个电脑上跑的,所以会出现这种情况。127.0.0.1是本机的环回地址。
端口号2381和我们程序中拿到的也是一样的。
扩展
服务端大致流程
在创建ServerSocket 实例的时候,他就已经监听了服务器本地的DEFAULT_PORT端口
serverSocket = new ServerSocket(DEFAULT_PORT);
进入构造函数(CTRL进入)
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
}
}
bind 函数
public void bind(SocketAddress endpoint, int backlog) throws IOException {
InetSocketAddress epoint = (InetSocketAddress) endpoint;
try {
getImpl().bind(epoint.getAddress(), epoint.getPort());
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");
Socket s = new Socket((SocketImpl) null);
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();
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
- https://www.jianshu.com/p/42918db85f19
- https://mp.weixin.qq.com/s/3Ma4nnkZNWiXacS7Ds2x4Q
|