基于TCP的Java网络编程

基于TCP的Java网络编程

1,TCP的工作方式

  TCP: Transmission Control Protocol,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,它能够做到两台机器之间进行可靠的、无差错的数据传输。

1.1 建立连接

  TCP是因特网中的传输层协议,使用三次握手协议建立连接。过程如下(以下内容来自百度百科):

  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。

    SYN:同步序列编号(*Synchronize Sequence Numbers*)。是TCP/IP建立连接时使用的握手信号。

  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。

    ACK :(Acknowledge character)即是确认字符,在TCP/IP协议中,如果接收方成功的接收到数据,那么会回复一个ACK数据。

  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established(建立连接)状态。

​                      图1 TCP的三次握手

 简单描述下这三次对话的过程:

  1. 客户端:服务端你在吗?我想给你发点学习资料,你要吗?
  2. 服务端:好啊,客户端你快发吧!
  3. 客户端:我现在就发,你准备接着吧。

 三次对话的目的是使数据包的发送和接受同步,经过三次对话之后,客户端向服务端正式发送数据。

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]

​                      图2 TCP的四次挥手

 简单描述下这三次对话的过程:

  1. 客户端:我发完了,服务端你收到了吗?收到就快去学习吧,拜拜。
  2. 服务端:收到了,但是还没有接受完,等等我。
  3. 服务端:好了,我接受完了,先去学习了,拜拜。
  4. 客户端:知道了,拜拜。

2,Java TCP编程概念

 Java里面的TCP编程的实现,可以分为以下四个步骤:

  1. 服务器:创建一个ServerSocket,等待连接
  2. 客户机:创建一个Socket,连接到服务器
  3. 服务器:ServerSocket接受到客户机的连接请求,创建一个Socket和客户端的Socket建立专线连接,后续服务器和客户机的对话会在一个单独的线程上运行。
  4. 服务器的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就可以退出群聊,不会影响其他的客户端。

  若有不足之处,还请各位大佬批评指正。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值