今天这篇博客主要是记录一下如何使用 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 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
os.write("\r\n".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 文件的内容如下:

Welcome!


Hello! I'm supermouse.

启动程序,在浏览器地址栏中输入:http://127.0.0.1:8888/NetWork/web/index.html,会看到浏览器显示如下: