2

基于TCP连接的Socket通信

TCP协议提供可靠的数据传输服务是通过建立TCP连接实现的。一条“TCP连接”连接的两端是Internet上分别在两台主机运行的两个进程,一个是发送进程,一个是接收进程,每个进程用一个Socket(IP地址和端口)唯一确定。一对Socket唯一标识一条TCP连接。TCP连接是全双工和点对点的,全双工指数据可双向传输,点对点是指每条TCP连接只有两个端点。

image.png

使用一对socket进行tcp连接,连接成功后,就可以通过socket发送或接收字节流数据,完成点到点、全双工的通信。

Java的TCP Socket通信

clipboard.png

服务端通过创建指定端口号的ServerSocket对象,调用accept()方法等待客户端连接请求,等待期间当前进程处于堵塞状态。当服务端接收到客户端的连接请求时,进程继续运行,并建立一条TCP连接,accept()完成并返回一个Socket对象,通过该Socket对象与客户端的Socket对象实现实时的数据通信。

ServerSocket serverSocket = new ServerSocket(7000);
Socket socket = serverSocket.accept();

客户端创建Socket对象,指定服务器主机的IP和端口,发出TCP连接请求,待服务器接受连接请求后Socket对象创建成功,此时可以通过此Socket对象与服务端Socket对象实时通信。

Socket socket = new Socket("127.0.0.1", 7000);

使用Socket对象输出/读取字节流数据

从控制台获取输入的字符串,字符串数据将从PrintWriter"流入"OutputStreamWriter在"流入"socket的OutputStream中,通过TCP建立的链路,流到与之对应的Socket的InputStream中。注意,Socket总是成对的。

PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
message = scanner.nextLine();
writer.println(message);
writer.flush();

在另一端的Socket中,便可以通过InputStream把发送的数据读取出来,此时数据的流向为InputStream->InputStreamReader->BufferedReader->控制台。

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = "";
while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
}

整个的数据流向为:客户端控制台->PrintWriter->OutputStreamWriter->客户端Socket的OutputStream->TCP链路->服务端Socekt的InputStream->InputStreamReader->BufferedReader->服务端控制台。通过socket便可以轻易的将数据从客户端发送到服务端了。

示例

了解了Socekt通信后,我们可以使用Socket仿照QQ群聊效果:当某个用户发送消息时,所有用户都接收到此消息并显示到控制台上。

客户端

客户端需要两个进程,一个进程不停的从控制台中读取数据,读取到用户输入时即向服务端发送读取到的数据,把它称为写进程。另一个进程接收服务端发送的数据,并把它显示到控制台上,称为读进程。

public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(System.in);
        Socket socket = new Socket("127.0.0.1", 7000);

        // 获取输入流和输出流
        PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        // 开启两个线程分别读写
        
        // 写线程
        new Thread(() -> {
            // 循环从控制台读取数据
            String message = "";
            while (!message.equals("exit")) {
                message = scanner.nextLine();
                writer.println(message);
                writer.flush();
            }
            writer.close();
        }).start();

        // 读线程
        new Thread(() -> {
            // 不停从input流获取数据
            String line = "";
            try {
                while ((line = bufferedReader.readLine()) != null) {
                    System.out.format("%50s", line);
                    System.out.println();
                }
                bufferedReader.close();
            } catch (IOException e) {

            }
        }).start();

    }

服务端

为了响应多个客户端连接,需要在循环中不停的调用accept()方法,每当获取到一个新TCP连接时,把获取到的Socket对象存入set中,对每个Socket对象都开启一个线程,主要的任务是不停的从InputStream中读取数据,即接收发送客户端数据。获取到数据后,再发送给所有已连接的客户端Socket(除了发送数据的客户端Socket),即遍历set发送数据。

public class Server {
    public static int userCount = 0;

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(7000);
        Set<Socket> socketSet = new HashSet<>();

        while (true) {
            Socket socket = serverSocket.accept();
            socketSet.add(socket);
            userCount++;
            // 开启一个新线程
            Thread thread = new Thread(() -> {
                // 不停从input流获取数据
                BufferedReader bufferedReader = null;
                try {
                    bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    String userName = Thread.currentThread().getName();
                    String line = "";
                    System.out.println("start Read");
                    while ((line = bufferedReader.readLine()) != null) {
                        System.out.println(userName + "消息:" + line);
                        for (Socket tem :
                                socketSet) {
                            if (!tem.equals(socket)) {
                                PrintWriter writer = new PrintWriter(new OutputStreamWriter(tem.getOutputStream()));
                                writer.println(line + ":" + userName);
                                writer.flush();
                            }
                        }
                    }
                    bufferedReader.close();
                } catch (IOException e) {
                }
                System.out.println("finish Read");
            });
            thread.setName("用户" + userCount);
            thread.start();
        }

    }
}

在终端运行的效果图:
image.png
image.png
image.png


鲸冬香
456 声望27 粉丝