今天这篇博客主要是记录一下如何使用 Java 编写 TCP 网络通信程序,然后实现一个文件上传程序和一个简易的 HTTP 服务器。
首先说一下 TCP 通信的过程。服务端程序监听在服务器的某一个端口上,等待客户端的连接,负责监听的是监听套接字,可以称之为 listen_socket,当有客户端连接时,操作系统会创建一个新的套接字 connect_socket,这个套接字专门负责与刚刚连接的客户端进行通信,listen_socket 则继续监听。
客户端要想与服务端进行通信,首先需要创建一个 Socket,然后指定服务器的 IP 地址和端口号进行连接。
服务端
服务端的实现步骤如下:
- 创建一个 ServerSocket 对象,指定端口号。
- 使用 ServerSocket 对象中的 accept 方法,获取到请求的客户端 Socket。
- 使用 Socket 对象中的 getInputStream 方法,获取到网络字节输入流 InputStream 对象。
- 使用 InputStream 对象中的 read 方法读取客户端发送的数据。
- 使用 Socket 对象中的 getOutputStream 方法,获取到网络字节输出流 OutputStream 对象。
- 使用 OutputStream 对象中的 write 方法,给客户端发送数据。
- 释放资源(关闭 Socket 和 SocketServer)。
代码如下:
package com.company;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
Socket socket = server.accept();
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
OutputStream os = socket.getOutputStream();
os.write("收到,谢谢!".getBytes());
socket.close();
server.close();
}
}
客户端
客户端的实现步骤如下:
- 创建一个客户端 Socket 对象,构造方法中绑定服务器的 IP 地址和端口号。
- 使用 Socket 对象中的 getOutputStream 方法,获取网络字节输出流 OutputStream 对象
- 使用 OutputStream 对象中的 write 方法,给服务器发送数据。
- 使用 Socket 对象中的 getInputStream 方法,获取网络字节输入流 InputStream 对象。
- 使用 InputStream 对象中的 read 方法读取服务器反馈的数据。
- 释放资源(关闭 Socket)。
代码如下:
package com.company;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream();
os.write("你好,服务器。".getBytes());
InputStream is = socket.getInputStream();
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes, 0, len));
socket.close();
}
}
依次启动服务端和客户端程序,我们会看到下面的输出结果。
实战
文件上传
下面实现一个简单的文件上传程序。代码比较简单,而且注释很详细,就不再赘述了。
服务端代码:
package com.company.fileupload;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;
public class Server {
public static void saveFile(Socket socket) {
try {
//使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//判断“/tmp/upload/”这个目录是否存在,不存在则创建
File file = new File("/tmp/upload/");
if (!file.exists()){
file.mkdirs();
}
//为防止文件重名,将文件命名为“supermouse+时间戳+随机数”的格式
String fileName = "supermouse" + System.currentTimeMillis() + new Random().nextInt(9999) + ".jpg";
//创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
FileOutputStream fos = new FileOutputStream(file + "/" + fileName);
//使用网络字节输入流InputStream对象中的方法read读取客户端上传的文件
int len = 0;
byte[] bytes = new byte[1024];
System.out.println("服务端开始向硬盘写入文件,文件名:" + fileName);
while ((len = is.read(bytes)) != -1){
//使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器的硬盘上
fos.write(bytes, 0, len);
}
System.out.println("服务端读取文件完毕!");
//使用Socket对象中的getOutputStream,获取到网络字节输出流OutputStream对象
//使用网络字节输出流OutputStream对象中的方法write,给客户端回写“上传成功”
socket.getOutputStream().write("上传成功!".getBytes());
//释放资源(关闭FileOutputStream、Socket、ServerSocket)
fos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//创建一个服务器ServerSocket对象,指定端口号8888
ServerSocket server = new ServerSocket(8888);
/**
* 使用死循环,让服务器一直处于监听状态
*/
while (true){
//使用ServerSocket对象中的方法accept,获取到请求的客户端Socket
Socket socket = server.accept();
new Thread(() -> saveFile(socket)).start(); //当有客户端连接时,创建一个新的线程处理客户端请求
}
}
}
客户端代码:
package com.company.fileupload;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
//创建本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("/home/supermouse/1.jpg");
//创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
//使用Socket中的getOutputStream,获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1){
//使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
os.write(bytes, 0, len);
}
socket.shutdownOutput(); //关闭此套接字的输出流(这一步很重要,如果缺少了,服务器读取文件之后将进入阻塞状态)mmmmmm
//使用Socket中的getInputStream,获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
System.out.println("文件发送完毕!");
//使用网络字节输入流InputStream对象中的方法read读取服务器回写的数据
while((len = is.read(bytes)) != -1){
System.out.println(new String(bytes, 0, len));
}
//释放资源(关闭FileInputStream、Socket)
fis.close();
socket.close();
}
}
HTTP 服务器
接下来我们要实现的 HTTP 服务器其实逻辑上和刚才那个文件上传程序差不多,区别在于刚才的文件上传程序是客户端发送文件,服务端接收文件,这个 HTTP 服务器刚好相反,需要给客户端发送文件(包括 html 文件、图片等)。那么服务器怎么知道该给客户端发送哪个文件呢?这就需要解析 HTTP 的请求信息了。
由于 HTTP 协议很复杂,具体细节就不再多说了,你只需要知道,当我们在浏览器中输入一个 URL 的时候,服务器接收到的 HTTP 请求的第一行就会包含这个 URL 的一部分,比如我在浏览器中输入:http://127.0.0.1:8888/NetWork/web/index.html
,其中 NetWork 是 Java 项目的名称。
项目目录结构如下:
HTTP 请求的第一行就会是这样的信息:GET /NetWork/web/index.html HTTP/1.1
,那么我们只要把 NetWork/web/index.html
提取出来,就可以把相应的文件返回给客户端(也就是浏览器)了。
代码如下:
package com.company.BSTCP;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
* HTTP 服务器
*/
public class HTTPServer {
public static void handleRequest(Socket socket){
try {
//使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
//把网络字节输入流对象is转换为字符缓冲输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
//把客户端请求信息的第一行读取出来,即:GET /NetWork/web/index.html HTTP/1.1
String line = br.readLine();
System.out.println(line);
//把读取的信息进行切割,只要中间的部分,即:/NetWork/web/index.html
String[] arr = line.split(" ");
//获取当前项目根目录的父目录
String projectParent = new File(new File("").getCanonicalPath()).getParent();
//创建一个本地字节输入流,构造方法中绑定要读取的html文件路径
FileInputStream fis = new FileInputStream(projectParent + arr[1]);
//使用Socket中的方法getOutputStream获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
//写入Http协议响应头(固定写法)
os.write("HTTP/1.1 200 OKrn".getBytes());
os.write("Content-Type:text/htmlrn".getBytes());
os.write("rn".getBytes()); //必须写入空行,否则浏览器不解析
//一读一写复制文件,把文件读取的html文件发送给客户端
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1){
os.write(bytes, 0, len);
}
//释放资源
fis.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
//创建一个服务器ServerSocket对象,指定端口号8888
ServerSocket server = new ServerSocket(8888);
while (true) {
//使用ServerSocket对象中的方法accept,获取到请求的客户端Socket(这里的客户端是浏览器)
Socket socket = server.accept();
new Thread(() -> handleRequest(socket)).start();
}
}
}
有一个地方需要提一下,就是如果 index.html 文件中有图片的话,浏览器会再开一个线程来请求图片文件。
我的 index.html 文件的内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Welcome!</title>
</head>
<body>
<h1>Hello! I'm supermouse.</h1>
<img src="tree.jpg">
</body>
</html>
启动程序,在浏览器地址栏中输入:http://127.0.0.1:8888/NetWork/web/index.html
,会看到浏览器显示如下: