从01开始 从01开始
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)

peterjxl

人生如逆旅,我亦是行人
首页
  • 计算机科学导论
  • 数字电路
  • 计算机组成原理

    • 计算机组成原理-北大网课
  • 操作系统
  • Linux
  • Docker
  • 计算机网络
  • 计算机常识
  • Git
  • JavaSE
  • Java高级
  • JavaEE

    • Ant
    • Maven
    • Log4j
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • Servlet
  • Spring
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC
  • SpringBoot
  • 学习网课的心得
  • 输入法
  • 节假日TodoList
  • 其他
  • 关于本站
  • 网站日记
  • 友人帐
  • 如何搭建一个博客
GitHub (opens new window)
  • JavaSE

    • 我的Java学习路线
    • 安装Java
    • Java数据类型

    • Java多版本配置
    • 面向对象

    • Java核心类

    • IO

    • Java与时间

    • 异常处理

    • 哈希和加密算法

    • Java8新特性

    • 网络编程

      • TCP编程
        • Socket
        • 服务器端
        • 客户端
        • 演示
        • Socket流
        • 小结
      • UDP编程
      • 发送Email
      • 接受邮件
      • HTTP编程
      • RMI远程调用
    • Java
  • JavaSenior

  • JavaEE

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java并发

  • Java源码

  • JVM

  • 韩顺平

  • Java
  • Java
  • JavaSE
  • 网络编程
2023-04-17
目录

TCP编程

# 10.TCP编程

本文简单说下TCP编程

# Socket

在开发网络应用程序的时候,我们又会遇到Socket这个概念。Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络:

┌───────────┐                                   ┌───────────┐
│Application│                                   │Application│
├───────────┤                                   ├───────────┤
│  Socket   │                                   │  Socket   │
├───────────┤                                   ├───────────┤
│    TCP    │                                   │    TCP    │
├───────────┤      ┌──────┐       ┌──────┐      ├───────────┤
│    IP     │←────→│Router│←─────→│Router│←────→│    IP     │
└───────────┘      └──────┘       └──────┘      └───────────┘
1
2
3
4
5
6
7
8
9

Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。

为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。

一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开,例如:

  • 101.202.99.2:1201
  • 101.202.99.2:1304
  • 101.202.99.2:15000

使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。

因此,当Socket连接成功地在服务器端和客户端之间建立后:

  • 对服务器端来说,它的Socket是指定的IP地址和指定的端口号;
  • 对客户端来说,它的Socket是它所在计算机的IP地址和一个由操作系统分配的随机端口号。

‍

‍

# 服务器端

要使用Socket编程,我们首先要编写服务器端程序。Java标准库提供了ServerSocket​来实现对指定IP和指定端口的监听。ServerSocket​的典型实现代码如下:

package chapter20;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class TCPDemo1Server {
    public static void main(String[] args) throws Exception{
        ServerSocket ss = new ServerSocket(7777);
        System.out.println("server is running");
        while (true){
            Socket sock = ss.accept();
            System.out.println("connected from " + sock.getRemoteSocketAddress());
            Thread t = new Handler(sock);
            t.start();
        }
    }
}


class Handler extends Thread{
    Socket sock;

    public Handler(Socket sock){
        this.sock = sock;
    }

    @Override
    public void run() {
        try(InputStream input = this.sock.getInputStream();
            OutputStream output = this.sock.getOutputStream()){
            handle(input, output);
        }catch (Exception e){
            System.out.println("Client disconnected: ");
            e.printStackTrace();
            try{
                this.sock.close();
            }catch (Exception e2){
                System.out.println("sock close error: ");
                e2.printStackTrace();
            }
        }
    }

    private void handle(InputStream input, OutputStream output) throws IOException{
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        writer.write("hello!\n");
        writer.flush();
        while (true){
            String s = reader.readLine();
            if(s.equals("bye, server")){
                writer.write("bye, client!");
                writer.flush();
                break;
            }

            writer.write("server successfully receive message \" " + s + " \" from client. \n");
            writer.flush();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

‍

我们解读下这段代码。首先,服务器端通过代码:

ServerSocket ss = new ServerSocket(7777);
1

在指定端口7777​监听。这里我们没有指定IP地址,表示在计算机的所有网络接口上进行监听。

‍

如果ServerSocket​监听成功,我们就使用一个无限循环来处理客户端的连接:

 while (true){
    Socket sock = ss.accept();
    System.out.println("connected from " + sock.getRemoteSocketAddress());
    Thread t = new Handler(sock);
    t.start();
}
1
2
3
4
5
6

注意到代码ss.accept()​表示每当有新的客户端连接进来后,就返回一个Socket​实例,这个Socket​实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket​创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。

我们在多线程编程的章节中介绍过线程池,这里也完全可以利用线程池来处理客户端连接,能大大提高运行效率。

如果没有客户端连接进来,accept()​方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket​会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()​就可以获取新的连接。

‍

然后handle​函数就可以通过IO来读取(reader)客户端发送的数据,并通过IO返回数据给客户端(writer)。如果客户端发送了字符串bye, server​,则认为客户端要停止连接,跳出循环不再接受数据。

‍

‍

# 客户端

相比服务器端,客户端程序就要简单很多。一个典型的客户端程序如下:

package chapter20;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class TCPDemo2Client {
    public static void main(String[] args) throws IOException{
        Socket sock = new Socket("localhost", 7777);
        try(InputStream input = sock.getInputStream();
            OutputStream output = sock.getOutputStream()){
            handle(input, output);
        }
        sock.close();
        System.out.println("Disconnected from server.");
    }

    private static void handle(InputStream input, OutputStream output) throws IOException{
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
        BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server] " + reader.readLine());
        while(true) {
            System.out.print(">>> ");
            String s= scanner.nextLine();
            writer.write(s);
            writer.newLine();
            writer.flush();
            String resp = reader.readLine();
            System.out.println("<<< " + resp);
            if(resp.equals("bye, client!")){
                break;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

客户端程序通过:

Socket sock = new Socket("localhost", 7777);
1

连接到服务器端,注意上述代码的服务器地址是"localhost"​,表示本机地址,端口号是7777​。如果连接成功,将返回一个Socket​实例,用于后续通信。

‍

# 演示

我们运行TCPDemo1Server​,然后再运行TCPDemo2Client​,测试发送一些字符串给服务器:

​

‍

​

可以看到服务器正常接受了数据,并返回了接受到的数据是什么。

‍

‍

# Socket流

当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket​实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream​和OutputStream​来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:

// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
1
2
3
4

最后我们重点来看看,为什么写入网络数据时,要调用flush()​方法。

如果不调用flush()​,我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()​强制把缓冲区数据发送出去。

‍

# 小结

使用Java进行TCP编程时,需要使用Socket模型:

  • 服务器端用ServerSocket​监听指定端口;
  • 客户端使用Socket(InetAddress, port)​连接服务器;
  • 服务器端用accept()​接收连接并返回Socket​;
  • 双方通过Socket​打开InputStream​/OutputStream​读写数据;
  • 服务器端通常使用多线程同时处理多个客户端连接,利用线程池可大幅提升效率;
  • ​flush()​用于强制输出缓冲区到网络。

‍

‍

在GitHub上编辑此页 (opens new window)
上次更新: 2023/4/18 09:35:05
Optional
UDP编程

← Optional UDP编程→

Theme by Vdoing | Copyright © 2022-2023 粤ICP备2022067627号-1 粤公网安备 44011302003646号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式