基于TCP的Java网络编程
1,TCP的工作方式
TCP: Transmission Control Protocol,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,它能够做到两台机器之间进行可靠的、无差错的数据传输。
1.1 建立连接
TCP是因特网中的传输层协议,使用三次握手协议建立连接。过程如下(以下内容来自百度百科):
![](https://img-blog.csdnimg.cn/img_convert/bebd814b28bf0eab871d7e5604962f83.png)
图1 TCP的三次握手
简单描述下这三次对话的过程:
- 客户端:服务端你在吗?我想给你发点学习资料,你要吗?
- 服务端:好啊,客户端你快发吧!
- 客户端:我现在就发,你准备接着吧。
三次对话的目的是使数据包的发送和接受同步,经过三次对话之后,客户端向服务端正式发送数据。
1.2 断开连接
建立一个连接需要三次握手,而终止一个连接要经过四次挥手。具体过程如下:
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]
![](https://img-blog.csdnimg.cn/img_convert/485cc7172e682b8e4f9e1f0a4311db15.gif)
图2 TCP的四次挥手
简单描述下这三次对话的过程:
- 客户端:我发完了,服务端你收到了吗?收到就快去学习吧,拜拜。
- 服务端:收到了,但是还没有接受完,等等我。
- 服务端:好了,我接受完了,先去学习了,拜拜。
- 客户端:知道了,拜拜。
2,Java TCP编程概念
Java里面的TCP编程的实现,可以分为以下四个步骤:
- 服务器:创建一个ServerSocket,等待连接
- 客户机:创建一个Socket,连接到服务器
- 服务器:ServerSocket接受到客户机的连接请求,创建一个Socket和客户端的Socket建立专线连接,后续服务器和客户机的对话会在一个单独的线程上运行。
- 服务器的ServerSocket继续等待连接,返回1。
上面会用到两个关键类:
-
ServerSocket:
服务端创建的,用来接受所有的客户端的Socket的请求。它需要绑定在一个端口上,如果不指定IP地址默认就是本机,如果机器上有多块网卡,需要指定一个IP地址。
-
Socket:
沟通服务端和客户端的运输通道,客户端需要绑定服务器的IP和Port。客户端往Socket输入流写入数据,送到服务端;客户端从Socket输出流取服务端过来的数据,服务端反之亦然。
客户端的输出流就是服务端的输入流,服务端的输出流就是客户端的输入流。
关于客户端和服务端有如下规则:
- 服务端等待响应时,处于阻塞状态。
- 服务端可以同时响应多个客户端
- 服务端每接受一个客户端,就启动一个独立的线程与之对应
- 客户端和服务端都可以选择关闭这对Socket通道
3,Java TCP编程实现
TcpServer:
package tcp;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(8001); // 驻守在8001端口
Socket socket = server.accept(); // 阻塞等待客户端连接上来
System.out.println("welcome to the java world");
InputStream ips = socket.getInputStream(); // 有客户端连接上来,打开输入流
OutputStream ops = socket.getOutputStream(); // 打开输出流
// 同一个Socket,客户端的输出流就是服务端的输入流,服务端的输出流就是客户端的输入流。
ops.write("Hello, Client!".getBytes()); // 输出一句话给客户端
// 对输入流进行封装 当然也可以对输出流进行封装 加快读写速度
BufferedReader br = new BufferedReader(new InputStreamReader(ips));
// 从客户端读取一句话 并输出
System.out.println("Client said: " + br.readLine());
ips.close();
ops.close();
socket.close();
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
TcpClient:
package tcp;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
try {
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 8001); // 向服务端请求连接
InputStream ips = socket.getInputStream();
BufferedReader brNet = new BufferedReader(new InputStreamReader(ips));
OutputStream ops = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(ops);
BufferedReader brKey = new BufferedReader(new InputStreamReader(System.in));// 定义键盘的输入流
while (true) {
String str = brKey.readLine();
if (str.equalsIgnoreCase("quit")) {
break;
} else {
System.out.println("I want to send: " + str);
dos.writeBytes(str + System.getProperty("line.separator"));
System.out.println("Server said: " + brNet.readLine());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行结果:
这里的TcpServer比较简单只是进行一次对话就终止了,实际上TcpServer应该一直驻守在那里,不断地接受客户端的请求,并启动一个独立的线程去对应它(新客户端),修改服务端代码如下:
package tcp;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(8001); // 驻守在8001端口
while (true) { // 不停的处理新的客户端的连接
Socket socket = server.accept();
System.out.println("来了一个client");
new Thread(new Worker(socket)).start(); // 启动一个线程
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Worker implements Runnable {
private Socket socket;
public Worker1(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
System.out.println("服务人员以启动");
try {
InputStream ips = socket.getInputStream();
OutputStream ops = socket.getOutputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(ips));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(ops));
while (true) {
String str = br.readLine();
System.out.println("client said: " + str);
if (str.equalsIgnoreCase("quit")) {
break;
}
// 回给客户端一句话
// 如果将输出流封装为BufferedWriter的对象,在写的时候要按照一下三步进行,以免出错。
bw.write("java 666" );
bw.newLine();
bw.flush();
}
br.close();
bw.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面就演示了一个服务端对应两个客户端,客户端发的所有消息服务端都能够收到,并且服务端都能够进行回馈。当然这个程序还有点不足,就是它不是一个聊天室功能,也就是说第一个客户端发的消息,第一个客户端看不到,这里并没有实现这个功能,下面将介绍聊天室功能的实现。
4,Java聊天室实现
Server:
package chart;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class TcpServer {
// 用来存放所有连接到服务器的客户端的socket信息,以便服务器给所有的客户端发消息
private static List<Socket> clientList = new ArrayList<>();
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(8001);
/* 由于每个Client连接上来时,我们都要创建一个新的线程,这样Client一多开销会比较大
所以这里使用线程共享池Executor, 用固定的线程来执行很多的任务 */
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(8);
while (true) {
Socket socket = server.accept();
System.out.println("客户端: " + socket.getPort() + " 连接成功");
clientList.add(socket);
Worker worker = new Worker(socket, clientList);
executor.submit(worker);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Worker:
package chart;
import java.io.*;
import java.net.Socket;
import java.util.List;
/**
* @author antique
*
* Worker 需要做到事情就是将接受到的某个客户端发来的消息
* 转发给其他所有的客户端
*/
public class Worker implements Runnable{
private final Socket socket;
private final List<Socket> clientList;
public Worker(Socket socket, List<Socket> clientList) {
this.socket = socket;
this.clientList = clientList;
}
@Override
public void run() {
try {
BufferedReader reader =
new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = null;
while (true) {
String str = reader.readLine();
System.out.println(socket.getPort() + ": " + str);
if (str.equalsIgnoreCase("quit")) {
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write("quit");
writer.newLine();
writer.flush();
break;
} else {
for (Socket client : clientList) {
// if (client != socket) { // 某个客户端发来的消息,不用再次转发给它
writer = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
writer.write(socket.getPort() + ": " + str);
writer.newLine();
writer.flush();
// }
}
}
}
reader.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Client:
package chart;
import java.io.*;
import java.net.Socket;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class TcpClient {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8001);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newScheduledThreadPool(2);
Send send = new Send(socket, writer);
Recv recv = new Recv(socket, reader);
executor.submit(recv);
executor.submit(send);
do {
Thread.sleep(1000);
} while (executor.getActiveCount() > 1);
writer.close();
reader.close();
socket.close();
executor.shutdown();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
class Send implements Runnable { // 发送线程
private final Socket socket;
private final BufferedWriter writer;
public Send(Socket socket, BufferedWriter writer) {
this.socket = socket;
this.writer = writer;
}
@Override
public void run() {
BufferedReader brKey = new BufferedReader(new InputStreamReader(System.in));
try {
while (true) {
String msg = brKey.readLine();
writer.write(msg);
writer.newLine();
writer.flush();
if (msg.equalsIgnoreCase("quit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Recv implements Runnable { // 接受线程
private final Socket socket;
private final BufferedReader reader;
public Recv(Socket socket, BufferedReader reader) {
this.socket = socket;
this.reader = reader;
}
@Override
public void run() {
try {
while (true) {
String msg = reader.readLine();
System.out.println(msg);
if (msg.equalsIgnoreCase("quit")) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试结果如图:
这里我只启动了两个客户端,效果如上,大家也可以多启动几个客户端试一试。输入quit
就可以退出群聊,不会影响其他的客户端。
若有不足之处,还请各位大佬批评指正。