Java初识网络编程

一:网络编程

(1)概念

指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)

(2)发送端和接收端

💛①发送端:在一次网络数据传输时,数据的发送方进程,称为发送端


💙②接收端:在一次网络数据传输时,数据的接收方进程,称为接收端


💚③收发端:在一次网络数据传输时,发送端和接收端两端,也简称为收发端

(3)请求和响应

一般来说,获取一个网络资源,涉及到两次网络数据传输:

第一次:请求数据的发送

第二次:响应数据的发送


🌟举例:

好比人在快餐店点一份炒面

先要有人发起请求:点一份炒面

再有快餐店提供的对应响应:提供一份炒面

(4)客户端和服务端

①客户端:发送请求和获取服务的一方进程,称为客户端


②服务端:在常见的网络数据传输场景下,把处理请求和提供服务的一方进程,称为服务端 (服务端即对外提供服务)

③常见的场景:

1. 客户端先发送请求到服务端

2. 服务端根据请求数据,执行相应的业务处理

3. 服务端返回响应:发送业务处理结果

4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

(5)网络编程总图

二:UDP和TCP的初步认识

(1)Socket

💜我们要想实现网络编程,写一个应用程序,主要靠的是调用传输层提供的API接口

而传输层最主要的协议是UDP和TCP,因此,靠的主要就是UDP和TCP提供的API接口


💓Socket套接字主要针对传输层协议划分为UDP和TCP

UDP的API主要提供两个类:DatagramSocket和DatagramPacket

TCP的API主要提供两个类:ServerSocket和Socket


Socket套接字是由系统提供用于网络通信的技术

Socket套接字是基于TCP/IP协议的网络通信的基本操作单元

基于Socket套接字的网络程序开发就是网络编程

(2)TCP特点

①有连接:客户端和服务器之间,利用内存保存对方一端的信息,当双方都保存这个信息之后,就能建立连接了,对于TCP而言,必须要建立连接才能使用

💙举例:就像打电话一样,对方不接通,你们是无法通信的


②可靠传输:A给B发的消息不是100%能发到的,但是A会尽可能的把消息传给B,假设传输失败,A也能感知到失败;假设传输成功,A也能知道自己传输成功了

💙举例:就像打电话一样,当你说了话但是对方没有响应时,你就会知道此时信号有问题

(可靠传输会使传输效率降低)


③面向字节流:读写的基本单位是字节,类似于文件操作


④全双工:一个通道,可以双向通信

💙举例:类似于车道


⑤无大小限制

(比UDP好的地方,传输数据大小没有限制)


⑥数据传输过程出错,即发送方和接收方的校验和不同,会重新发送

(3)UDP特点

 ①无连接:客户端和服务器之间,利用内存保存对方一端的信息,当双方都保存这个信息之后,就能建立连接了,对于UDP而言,无须建立连接也能使用

💙举例:就像发微信一样,有他的好友就行,发消息不用经过同意


②不可靠传输:A给B发的消息不是100%能发到的,但是A会尽可能的把消息传给B,假设传输失败,A无法感知到失败

💙举例:就像发微信一样,当你发了一条消息但是对方没有响应,可能是已读不回,但你却不知道是没看到还是故意不回

(不可靠传输会使传输效率提高)


③面向数据报:读写的基本单位是数据报


④全双工:一个通道,可以双向通信

💙举例:类似于车道


⑤大小受限:UDP协议首部中有一个16位的最大长度

也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)

(使用UDP的DatagramPacket时给字节数组初始化的时候值不能超过64K)

-------------------------------------------------------------------------------------------------------------------------

(UDP缺陷:无法表示一个较大的数据段)

当我们要传输的数据长度大于2个字节的长度时:

①方法一:我们可以在应用层进行分包(拆成多个部分),然后再通过多个UDP数据段分别发送。接收方收到后,再把几个包重新拼接成完整的数据;一般不推荐,比较麻烦

②方法二:不适用 UDP协议了,改成 TCP协议!!!因为 TCP 中没有这样的长度限制


⑥数据传输过程出错,即发送方和接收方的校验和不同,直接丢弃,不会重新发送

(4)网络编程Socket常见异常

在写网络编程的时候,UDP和TCP都需要抛出SocketException异常

同时SocketException也属于IO异常


三:UDP数据报套接字编程

(1)DatagramSocket API

1.作用

DatagramSocket用于发送和接收UDP数据报


🌟注意:DatagramSocket其实是一个Socket对象

我们需要知道,操作系统使用文件这样的概念,是为了管理一些软硬件资源,而操作系统也是用文件这样的方式去管理网卡,而表示网卡的这类文件,我们就称为Socket文件


🌟注意:Java的这些Socket对象就对应系统里的这些Socket文件,也就是对应着网卡这类的文件!因此,要进行网络通信,就必须要有Socket对象!

2.构造方法

①DatagramSocket() :创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口

💚(一般用于客户端,客户端使用哪个端口,由系统来自动分配)


②DatagramSocket(int port): 创建一个UDP数据报套接字的Socket,绑定到本机指定的port端口

💚(一般用于服务端,服务端使用哪个端口,由自己手动分配)


💖问题:为什么客户端要用无参构造方法,服务端要用带参数port的构造方法???

💗解答:我们都知道端口号是用来区分不同程序的;对于服务器来说,要有一个固定的端口号,这样当客户端来找的时候就很方便。比如说我在学校饭堂专门卖炒饭,位于饭堂的7号窗口,我是固定不动的,我就相当于服务端,为同学们提供服务也就是提供炒饭,当同学们想吃炒饭,直接来7号窗口即可,方便又快捷;假设有个A同学来吃的时候,A同学去7号窗口拿完炒饭然后找个位置坐,A同学就相当于客户端,A同学想要获取炒饭也就是获取服务嘛,那么A同学找的位置就相当于客户端的端口,但是,A同学每次来的时候都能坐之前的那个位置吗?那个位置可能被别人坐了也不一定吧?因此,客户端的端口是没办法固定的

💚一个客户端的主机上面运行程序会很多,假设你手动指定端口号,那万一这个端口号被别的程序占用了呢?一个端口只能被一个进程占用,所以让系统自动分配客户端端口才是一个明智的选择!

3.普通方法

①void receive(DatagramPacket p):接收数据报

🌟这里的参数类型是DatagramPacket

💙这个参数DatagramPacket p是一个输出型参数,传入receive的是一个空对象,receive就会把这个空的对象的内容给填充上,当receive执行结束,于是就得到了一个装满内容的DatagramPacket

(如果客户端没有发送请求,也就是没有接收到数据报时,方法会阻塞等待)


②void send(DatagramPacket p):发送数据报

🌟这里的参数类型是DatagramPacket

(不会阻塞等待,直接发送)


③void close():关闭此数据报套接字

🌟当整个程序只有一个Socket对象时,不使用close关闭也没事,因为此时这个对象的生命周期很长,不是频繁创建的,会跟随着程序的关闭而结束

🌟当整个程序有多个Socket对象时,必须使用close,因为此时这个对象的生命周期很短,是频繁创建的

(2)DatagramPacket API

1.作用

DatagramPacket表示了一个UDP的数据报,也是用来发送和接收UDP数据报

2.构造方法

①DatagramPacket(byte[] buf, int length)

构造一个DatagramPacket以用来接收数据报

接收的数据保存在字节数组(第一个参数buf) (注意:这里的数组不能超过64k)

接收指定长度(第二个参数 length)


②DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):

构造一个DatagramPacket以用来发送数据报

发送的数据为字节数组(第一个参数buf)       (注意:这里的数组不能超过64k)

从offset到指定长度length

address指定目的主机的IP和端口号(构造UDP发送的数据报时,需要传入SocketAddress类的参数)


💗DatagramPacket这个对象是用来保存数据的内存空间,但它跟我们以往学的集合类不一样,以往的集合类是自动创建空间来保存,而DatagramPacket需要手动指定内存空间大小


💙DatagramPacket这个对象里就包含了通信双方的IP和端口号

(一般通过getSocketAddress()方法即可获取IP和端口号)


🌟DatagramPacket的构造方法创建好对象后,接收/发送数据都需要传给DatagramSocket的普通方法;DatagramPacket只是作为DatagramSocket的方法参数

🌟DatagramSocket 是取外卖和送外卖的外卖小哥, 而 DatagramPacket就是外卖

先有外卖,再把外卖给小哥;即有了DatagramPacket对象,再把它交给DatagramSocket

由外卖小哥即DatagramSocket负责去送外卖,即传输数据

3.普通方法

①InetAddress getAddress():

从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址

(获取对方的IP地址)

(假设在服务器中,它接收客户端的请求,即接收客户端的数据报,通过getAddress就可获得客户端的IP)


②int getPort():

从接收的数据报中,获取发送端主机端口号;或从发送的数据报中,获取接收端主机端口号

(获取对方的端口号)


③byte[] getData():获取数据报中的数据

(返回值是实际数据大小的字节数组)


④int getLength():获取数据报中的实际长度的数据


SocketAddress getSocketAddress():

获取要将此包发送到的或发出此数据报的远程主机的SocketAddress(通常为IP地址+端口号)

(3)InetSocketAddress API

1.作用

使用DatagramPacket API的发送数据报构造方法时,需要传入 SocketAddress

2.构造方法

①addr:IP地址

②port:端口号 


🌟InetSocketAddress是SocketAddress的子类


(4)InetAddress  API

1.静态方法

①InetAddress.getLocalHost():获取本机的InetAddress对象


②InetAddress.getByName(指定主机名):根据指定主机名IP获取InetAddress对象

(一般用于客户端发出请求时需要获取带有服务器IP的InetAddress对象)


③InetAddress.getByName(指定域名):根据域名返回InetAddress对象

2.InetAddress和InetSocketAddress的联系和区别

(1)联系

InetAddress和InetSocketAddress都是SocketAddress的子类


(2)区别

①InetAddress封装了计算机的ip地址和DNS,没有端口
②InetSocketAddress封装了计算机的ip地址和DNS,包括了端口

(5)实现UDP回显服务器

1.回显服务器(Echo Server)

客户端发啥,服务器回啥

2.客户端和服务器代码逻辑

客户端:

1.输入请求,将请求发送给服务器

2.接收服务器发送回来的响应

(先发后收)


服务器:

1. 接收并读取客户端发来的请求, 然后解析

2. 根据请求, 计算出响应(重点在于如何第二步计算)

3.把响应写回给客户端

(先收后发)


运行顺序:一定是服务器先启动,然后等待客户端启动和发送请求,顺序不能乱

3.服务器代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// UDP 的 回显服务器.
// 客户端发的请求是啥, 服务器返回的响应就是啥.
public class UdpEchoServer {
    private DatagramSocket socket = null;

    // 通过构造方法,指定服务器要绑定的端口
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    // 使用这个方法启动服务器.
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 使用while循环反复的, 长期的执行针对客户端请求处理的逻辑.
            // 一个服务器, 运行过程中, 要做的事情, 主要是三个核心环节.

            // 1. 接收和读取客户端发来的请求, 并解析
            // 接收的数据保存在byte[4096]数组,一次读取4096个字节长度
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            // 将保存数据的对象requestPacket传给DatagramSocket的receive方法作为参数,用来接收数据报
            socket.receive(requestPacket);
            //  这样的转字符串的前提是, 后续客户端发的数据就是一个文本的字符串.
            //  通过String构造方法将客户端发来的requestPacket它里面存储的是字节数组,将字节数组转为字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());


            // 2. 根据请求, 计算出响应
            // 下面我们自定义个porcess方法,用来计算
            // 响应的请求放到response
            String response = process(request);


            // 3. 把响应写回给客户端,即服务器发送数据报给客户端
            //    此时需要告知网卡, 要发的内容是啥, 要发给谁.
            //    这里我们使用getBytes()方法将字符串转换为字节数组
            //    为毛要转为字节数组??DatagramPacket构造方法参数类型要求的就是字节数组
            //    步骤一中的 requestPacket接收客户端发来的数据时就包括了客户端的IP和端口
            //    通过requestPacket.getSocketAddress得知客户端的IP和端口,此时就知道要发送给谁了
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requestPacket.getSocketAddress());
            socket.send(responsePacket);


            // 记录日志, 方便观察程序执行效果.
            // requestPacket.getAddress().toString()获取客户端的IP并打印
            // requestPacket.getPort()获取客户端的端口并打印
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }

    // 根据请求计算响应. 由于是回显程序, 响应内容和请求完全一样.
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);   //指定服务器的固定端口是9090
        server.start();
    }
}
4.客户端代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    public UdpEchoClient(String ip, int port) throws SocketException {
        // 这里的serverIp和serverPort其实是服务器的 ip 和 服务器的端口
        // 客户端只有连接上同一个端口即同一个程序才能获取服务器的服务
        serverIp = ip;
        serverPort = port;
        // 这个 new 操作, 因为现在我们写的是客户端,因此就不再指定端口了. 让系统自动分配一个空闲端口
        socket = new DatagramSocket();
    }

    // 让这个客户端反复的从控制台读取用户输入的内容. 把这个内容构造成 UDP 请求, 发给服务器. 再读取服务器返回的 UDP 响应
    // 最终再显示在客户端的屏幕上.
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        System.out.println("客户端启动!");
        while (true) {
            // 1. 从控制台读取用户输入的内容
            System.out.print("-> "); // ->是一个命令提示符, 提示用户要输入字符串.
            String request = scanner.next(); //用户输入请求


            // 2. 构造请求对象, 并发给服务器.
            // 因为用户输入的是字符串,传输的时候要转为字节数组,这里我们通过getBytes
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);


            // 3. 读取服务器的响应, 并解析出响应内容.
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 将服务器回应的内容转换成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());


            // 4. 显示到屏幕上.
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        //接下来客户端就访问这个9090的服务器端口,与9090端口的服务器进行通信
         UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
         client.start();
    }
}
5.运行结果

🌟先启动服务器,再启动客户端

 


💗通过IDEA还可以开启多个客户端

(6)实现UDP服务器的翻译功能

package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
    //定义一个HashMap获取对应的翻译
    private Map<String, String> dict = new HashMap<>();

    //直接使用继承的方式,避免了重复写代码
    //因此start方法就不要写了,直接就复用了之前的 start !
    public UdpDictServer(int port) throws SocketException {
        super(port);

        dict.put("communicate", "沟通;交流");
        dict.put("poole", "普尔");
        dict.put("dump", "丢弃;垃圾场");
        dict.put("dumb","哑的;无法讲话的");
        dict.put("drum","打鼓");
        
    }

    // 调整process方法
    @Override
    public String process(String request) {
        // 把请求对应单词的翻译, 给返回回去
        // 查不到就返回default值
        return dict.getOrDefault(request, "该词没有查询到!");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}

四:TCP流套接字编程

(1)ServerSocket API

1.作用

服务器端通过创建 ServerSocket 对象实现

(🌟只用于服务器)

2.构造方法

ServerSocket(int port):创建一个服务端流套接字Socket,并绑定到指定端口

3.普通方法

①Socket accept()

💜监听构造方法中指定的端口,当接收到客户端的连接请求后,会生成一个 Socket 对象与客户端连接,并返回此连接对应的 Socket 对象(可以看到accept方法的返回类型是Socket),我们一般会写一个Socket对象来接收,接下来就通过我们写的这个Socket对象来与客户端进行通信;反之,如果没有收到客户端的连接请求,就会阻塞等待

💚(这就是TCP与UDP不同的地方,前面我们提到过TCP的特点就是必须建立连接才能使用)


②void close():关闭此套接字

🌟当整个程序只有一个Socket对象时,不使用close关闭也没事,因为此时这个对象的生命周期很长,不是频繁创建的,会跟随着程序的关闭而结束

🌟当整个程序有多个Socket对象时,必须使用close,因为此时这个对象的生命周期很短,是频繁创建的

(2)Socket API

1.作用

它可以让客户端和服务器之间相互通信

(🌟既可用于客户端,也可用于服务器)

(🌟一般是用来创建客户端对象、建立连接)


💙①Socket可以是客户端的Socket

💚②Socket也可以是服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket,上面的accept方法就提到了


不管是客户端还是服务端的Socket,都是双方建立连接以后,保存的对端信息

(例如:服务端的Socket保存着客户端的IP和端口)

2.构造方法

Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

(创建一个客户端,系统随机分配客户端端口,然后与对应参数IP和端口的服务器连接)


🌟注:这里的IP和port都是服务器的ip和端口,因为客户端要发信息肯定得先知道服务器的位置

3.普通方法

①InetAddress getInetAddress():返回套接字所连接的IP地址


int getPort():返回套接字所连接的端口


InputStream getInputStream():返回此套接字的输入流


④OutputStream getOutputStream():返回此套接字的输出流


⭐1.Socket 对象内部包含了两个字节流对象, 可以把这两个字节流对象通过get方法(即getInputStream和getOutputStream)获取到, 完成后续的读写工作

⭐2.此时就不用new这两个字节流对象,而是直接get方法,于是就能够获取Socket对象中的读和写,就像耳机和麦克风一样,读就用耳机InputStream,写就用麦克风OutputStream


💙3.对于服务器来说,即读请求和写响应,随后就可直接在后面的读取请求中调用InputStream的相关方法和写回响应中调用OutputStream的相关方法


💚4.对于客户端来说,即写请求和读响应,随后就可直接在后面的写入请求中调用OutputStream的相关方法和读取响应中调用InputStream的相关方法就行


💛5.TCP主要是面对字节流,因此靠的是字节流的两个读写类

而UDP面对的是数据报,因此可通过专门的构造方法填写数据报,发送数据报,解析数据报

(3)ServerSocket和Socket的区别

1.Socket 是用于建立连接的类,它可以让客户端和服务器之间相互通信

2.ServerSocket 是用于监听连接请求的类,它全是应用在服务器,它在服务器端等待客户端的连接请求,并在连接成功后与客户端建立对应的 Socket 连接


3.当客户端与服务器建立连接时

①客户端通过创建 Socket 对象实现,目的在于建立客户端后连接服务器,主要是连接作用

②服务器端则通过创建 ServerSocket 对象实现,目的在于建立指定端口的服务器


4.ServerSocket是Socket的子类

(4)实现TCP回显服务器

1.客户端和服务器代码逻辑

客户端:

1.输入请求,将请求发送给服务器

2.接收服务器发送回来的响应

(写请求读响应)


服务器:

💚与UDP不同的是,这里需要先建立连接

1.建立与客户端的连接

2. 接收并读取客户端发来的请求, 然后解析

3. 根据请求, 计算出响应(重点在于如何第二步计算)

4.把响应写回给客户端

(读请求写响应)


运行顺序:一定是服务器先启动,然后等待客户端启动和发送请求,顺序不能乱

2.服务器代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    //通过ServerSocket来创建服务端的对象
    private ServerSocket serverSocket = null;
    // 因为考虑到客户端的请求可能有多个,线程资源开销比较大,可以使用线程池
    // 此处不应该创建固定线程数目的线程池,因为如果固定了线程,那就相当处理客户端的请求给了限制
    // 因此我们使用newCachedThreadPool来创建一个动态线程数目的线程池
    private ExecutorService service = Executors.newCachedThreadPool();


    // 通过构造方法初始化的方式,这个new操作就会使服务器绑定指定端口号
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }


    // 启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //使用while循环,接收多个客户端
        while (true) {
            //调用serverSocket.accept就可以监听客户端是否发出请求
            //如果发出请求,accept就会返回一个Socket对象
            //在这里我们创建一个Socket对象即clientSocket来接收accept返回的Socket对象
            //往后我们就通过clientSocket来与客户端进行通信
            Socket clientSocket = serverSocket.accept();

            // 使用多线程的原因在于,如果是单个线程,假设已经有一个客户端发来请求了,clientSocket对象只能处理这一个请求
            // 简单来说,就是不能使用多个客户端来发送请求,只能等一个请求解决完了才能接着下一个请求
            // 使用多线程后,accept就通过主线程不断接收客户端请求,即每次有一个客户端,就都创建一个新线程去服务
            // 这里我们不使用原始的线程创建方式,而是使用线程池, 这样就可以解决上述资源频繁创建删除问题
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();

            //通过调用submit方法来执行任务
            service.submit(new Runnable() {
                @Override
                public void run() {
                    //通过processConnection这个方法完成服务器的一系列操作
                    //根据参数clientSocket来与客户端通信
                    processConnection(clientSocket);
                }
            });

        }
    }

    // 通过这个processConnection方法来处理一个连接的逻辑
    private void processConnection(Socket clientSocket) {
        //因为有了返回的clientSocket,证明已经和客户端连接上了,这里我们打印一下客户端的相关信息
        //打印一下客户端的IP和端口
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());


        // 接下来就可以依次从①读取请求, ②根据请求计算响应, ③返回响应这三步走了.
        // Socket 对象内部包含了两个字节流对象, 可以把这俩字节流对象获取到, 完成后续的读写工作
        // 这里的Socket对象即clientSocket就包括了客户端发来的请求,我们可以通过InputStream读取
        // 使用try with resource自动close关闭资源
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 一次连接中, 可能会涉及到多次请求/响应,因此这里我们使用while循环
            while (true) {
                // 1. 读取请求并解析. 为了读取方便, 直接使用 Scanner,通过Scanner直接读取客户端发来的请求
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 如果读到空白符,返回false
                    // 读取完毕, 客户端下线.
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 这个代码暗含一个约定, 客户端发过来的请求, 得是文本数据, 同时, 还得带有空白符作为分割. (比如换行这种)
                // 因为next()只能读取空格前的数据
                // scanner.next()方法读取客户端请求
                String request = scanner.next();


                // 2. 根据请求计算响应
                String response = process(request);


                // 3. 把响应写回给客户端. 把 OutputStream 使用 PrintWriter 包裹一下, 方便进行发数据.
                // PrintWriter就可以直接从字符串角度写入,而不用强制转换成字节数组
                PrintWriter writer = new PrintWriter(outputStream);
                //    使用 PrintWriter 的 println 方法, 把响应返回给客户端.
                //    此处用 println, 而不是 print 就是为了在结尾加上 \n . 方便客户端读取响应, 使用 scanner.next 读取.
                writer.println(response);
                //    这里还需要加一个 "刷新缓冲区" 操作.
                writer.flush();


                // 日志, 打印当前的请求详情.
                System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 在 finally 中加上 close 操作, 确保当前 socket 被及时关闭!!
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}
3.客户端代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    //通过Socket来创建客户端的对象
    private Socket socket = null;


    // 要和服务器通信, 就需要先知道, 服务器所在的位置
    // 这里的serverIp和serverPort都是服务器的IP和端口
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 这个 new 操作,使此时的socket对象与服务器对应IP的主机和对应端口的进程建立连接
        // 连接完成之后, 就完成了 tcp 连接的建立
        // 此时服务器的accpet方法就会接收到感应
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动");

        //这里是客户端输入请求的scannerConsole
        Scanner scannerConsole = new Scanner(System.in);

        //使用try with resource自动close关闭资源
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 从控制台输入字符串.
                System.out.print("-> ");
                //客户端输入请求后用request保存
                String request = scannerConsole.next();


                // 2. 把请求request发送给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                //    使用 println 带上换行. 后续服务器读取请求, 就可以使用 scanner.next 来获取了
                printWriter.println(request);
                //    不要忘记 flush, 确保数据是真的发送出去了!!
                printWriter.flush();


                // 3. 从服务器读取响应.
                //这里的scannerNetwork是读取响应的Scanner对象
                Scanner scannerNetwork = new Scanner(inputStream);
                //通过next()读取响应,并用response保存
                String response = scannerNetwork.next();


                // 4. 把响应打印出来
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        //客户端通过连接服务器的指定端口9090
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值