今天这篇博客主要是记录一下如何使用 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 文件的内容如下:

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Welcome!</title></head><body><h1>Hello! Im supermouse.</h1><img src="tree.jpg"></body></html>

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

分类: 源码分享 标签: 暂无标签

评论

暂无评论数据

暂无评论数据

目录