跟着韩顺平老师学Java,通过一个简单案例来理解网络通信编程部分的Socket。
Socket基本介绍:
- 套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准;
- 通信的两端(服务器端、客户端)都要有Socket,是两台机器间通信的端点;
- 网络通信其实就是Socket间的通信;
- Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输;
示意图:
简易案例一:
- 编写一个服务器端和一个客户端
- 服务器端占用9999端口监听
- 客户端连接到服务器端,发送 “Hello,server, i am client ”,然后退出
- 服务器端接收到客户端发送的信息,输出,并退出;
服务端类:
public class ServerSimulate {
public static void main(String[] args) throws IOException {
// 在本机的9999端口监听,等待连接;
// 细节注意:要求9999端口未被占用,否则会报异常;
ServerSocket serverSocket = new ServerSocket(9999);
System.out.println("Sever begin...");
// 没有客户段连接时,程序会阻塞在此,等待连接;
// 如果有客户端连接,则会返回一个Socket对象,程序继续;
Socket socket = serverSocket.accept();
System.out.println("Sever accept...");
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
// 根据读取到的实际长度,显示内容
int readlen = 0;
while ((readlen = inputStream.read(buffer)) != -1)
{
System.out.println(new String(buffer, 0, readlen));
}
System.out.println("Sever closed...");
inputStream.close();
socket.close();
serverSocket.close();
// serverSocket 可以对应创建很多 socket (多个客户端来连接服务器);即 多并发
// 只要有一次 accept 就可以返回一个 socket;
}
}
细节理解:
- 服务器使用本机的9999端口进行监听,如果该端口已被占用,则无法使用该端口,会报异常。
- 服务器端通过 accept() 方法获取一个 Socket 对象,与连接该服务器的客户端的 Socket 对应。
- SeverSocket 和 Socket 的区别:当多个客户端来连接这个服务器时,这个服务器的SeverSocket 可以接收多个 Socket,也就是常说的 多并发。
客户端类:
public class ClientSimulate {
public static void main(String[] args) throws IOException {
// 连接服务器
// 这里由于是在一台主机上同时模拟服务器和客户端,所以通过InetAddress获取本机ip
// 如果要用其它服务器,则输入对应服务器的ip和端口号即可
// 如果连接成功,返回socket对象
Socket socket = new Socket(InetAddress.getLocalHost(),9999);
// 得到和socket关联的输出流对象
OutputStream outputStream = socket.getOutputStream();
// 通过输出流,写入数据到数据通道
outputStream.write("Hello,server, i am client".getBytes());
// 设置写入结束标记,表示此次传输完毕
socket.shutdownOutput();
// 关闭流和对象
outputStream.close();
socket.close();
}
}
细节理解:
- 客户端通过新建 Socket 来连接服务器,构造函数内包括所要连接的服务器的 IP地址、端口号;
- 在运行时,先运行服务器端,再运行客户端,才能正常连接。
- 在写入数据完毕后,要设置结束标记(socket.shutdownOutput() )。否则另一端会一直等待(以为还没传输完)。
- 如果使用的字符流,需要手动刷新(bufferWriter.flush() ),否则数据不会写入数据通道。字节流则无需此操作。
- 记得程序最后关闭相关流和socket对象,避免资源浪费。
升级案例二:
- 编写一个服务端和一个客户端
- 服务端在8888端口监听
- 客户端连接到服务端,发送一张图片
- 服务器端接收到客户端发送的图片。保存到 scr 下,给服务端发送“收到图片”再退出
- 客户端接收到服务端发送的“收到图片”,再退出
- 该程序使用自定义的 StreamUtils.java 工具类来直接使用
StreamUtils.java 工具类
/**
* 此类用于写关于流的读写算法
*/
public class StreamUtils {
/**
* 功能:将输入流转换为 byte[]
* @param inputStream
* @return
*/
public static byte[] streamToByteArray(InputStream inputStream) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();// 创建输出流对象
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer)) != -1)
{
bos.write(buffer, 0, len);
}
byte[] arr = bos.toByteArray();
bos.close();
return arr;
}
/**
* 功能:将输入流转换为字符串
* @param inputStream
* @return
* @throws IOException
*/
public static String streamToString(InputStream inputStream) throws IOException
{
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = br.readLine()) != null)
{
sb.append(line + "\r\n");
}
return sb.toString();
}
}
服务端类
public class ServerSimulate2 {
public static void main(String[] args) throws IOException {
// 服务器在本机监听888端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("Sever listen 8888 begin...");
// 等待连接
Socket socket = serverSocket.accept();
System.out.println("Sever accept...");
// 读取客户发送的数据
// 通过 Socket 得到输入流
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
byte[] bytes = StreamUtils.streamToByteArray(bis);
// 将得到的 byte 数组,写入到指定的路径,就得到一个文件了
String destFilePath = "src\\abc.jpg";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
bos.write(bytes);
bos.close();
// 向客户端回复“收到图片”
// 通过 socket 获取到输出流(字符)
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("收到图片");
bw.flush(); // 把内容刷新到数据通道
socket.shutdownOutput(); // 设置写入结束标记
System.out.println("Sever closed...");
bos.close();
bw.close();
socket.close();
serverSocket.close();
}
}
客户端类
public class ClientSimulate2 {
public static void main(String[] args) throws IOException {
// 客户端连接服务器的8888端口
Socket socket = new Socket(InetAddress.getLocalHost(),8888);
// 创建读取磁盘文件的输入流
String filePath = "C:\\CloudMusic\\sky.jpg";
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));
// bytes 就是图片对应的字节数组
byte[] bytes = StreamUtils.streamToByteArray(bis);
// 得到和socket关联的输出流对象
OutputStream outputStream = socket.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(outputStream);
bos.write(bytes);
bis.close();
socket.shutdownOutput(); // 设置写入数据的结束标记
// 接收从服务端回复的消息
InputStream inputStream = socket.getInputStream();
// 使用工具类直接将输入流读取到的内容转成字符
System.out.println(StreamUtils.streamToString(inputStream));
// 关闭流和对象
bos.close();
inputStream.close();
socket.close();
}
}