Java网络编程

一、网络基本介绍

  随着时代的发展,越来越需要计算机之间互相通信,共享软件和数据,即以多个计算机协同工作来完成业务,就有了网络互连。
  网络互连:将多台计算机连接在一起,完成数据共享
  数据共享本质是网络数据传输,即计算机之间通过网络来传输数据,也称为网络通信。
  根据网络互连的规模不同,可以划分为局域网广域网

(一)局域网(LAN)

局域网,即 Local Area Network,简称LAN
  Local 即标识了局域网是本地,局部组建的一种私有网络
  局域网内的主机之间能方便的进行网络通信,又称为内网;局域网和局域网之间在没有连接的情况下,是无法通信的
局域网组建网络的方式有很多种:

1. 基于网线直连

即主机和主机之间直接通过网线连接

2. 基于集线器连接

  集线器现在用的已经比较少了,主要就是因为它获得数据之后,发送数据并没有针对性,而是采用广播方式发送,也就是说当它要向某节点发送数据时,不是直接把数据发送到目的节点,而是把数据包发送到与集线器相连的所有节点
  由于集线器会把收到的任何数字信号,经过再生或放大,再从集线器的所有端口提交,这会造成信号之间碰撞的机会很大,而且信号也可能被窃听,并且这代表所有连到集线器的设备,都是属于同一个碰撞域名以及广播域名,因此大部份集线器已被交换机取代

3. 基于交换机组建

4. 基于交换机和路由器组建

  交换机(Switch)意为“开关”是一种用于电(光)信号转发的网络设备。它可以为接入交换机的任意两个网络节点提供独享的电信号通路。最常见的交换机是以太网交换机。交换机是把若干设备组件到一个局域网中
  路由器(Router)是连接两个或多个网络的硬件设备,在网络间起网关的作用,是读取每一个数据包中的地址然后决定如何传送的专用智能性的网络设备。它能够理解不同的协议。路由器存在两类端口,WAN口和LAN口,其中插在LAN口上的设备,在一个局域网里,通过WAN口连接到另一个局域网。路由器则是连接了两个局域网
  关于交换机和路由器在网络通信中是如何具体的发挥作用的,我们在下面介绍

(二)广域网(WAN)

广域网,即 Wide Area Network,简称WAN
  通过路由器,将多个局域网连接起来,在物理上组成很大范围的网络,就形成了广域网。广域网内部的局域网都属于其子网
  如果属于全球化的公共型广域网,则称为互联网(又称公网,外网),属于广域网的一个子集。
  有时在不严格的环境下说的广域网,其实是指互联网,广域网和局域网都只是一个相对的概念,广域网其实也就是一个比较大的局域网

二、网络通信基础

  网络互连的目的是进行网络通信,也即是网络数据传输,更具体一点,是网络主机中的不同进程间,基于网络传输数据
  在组建的网络中,判断数据是从哪里传向哪里,就需要使用IP地址来标识

(一)IP地址

1.概念

  IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址

2.格式

  IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)
  通常采用“点分十进制”的方法表示,即a.b.c.d的形式(a,b,c,d都是0~255之间的十进制整数),比如:127.0.0.1

3.特殊IP

127.0.0.1用于本机环回测试,即自己通过网络访问自己的本机地址可以直接通过该IP地址进行访问

IP地址解决了网络通信时,定位网络主机的问题,要想让数据传到主机内部后知道该由哪个进程来接收,就需要端口号来识别

(二)端口号

1.概念

  端口,就像是门牌号一样,一个主机中的每个应用程序对应一个端口号,通过端口号,数据才能真正知道该由哪个应用程序处理

2.格式

  端口号是0~65535范围的数字,在网络通信中,进程可以通过绑定一个端口号,来发送及接收网络数据

3.注意事项

  两个不同的进程,不能绑定同一个端口号,但一个进程可以绑定多个端口号

问题:当数据知道该从哪来到哪去,但是网络通信是基于二进制0/1数据来传输,那么如何告诉对方发送的数据是什么样的?况且我们知道计算机是存在好多种编码方式的,那么该如何区分是哪一种
  因此我们就需要使用协议来规定双方的数据格式

(三)协议

1.概念

  网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合,只有遵守这个约定,计算机之间才能相互通信交流。通常由三要素组成:

  1. 语法:即数据与控制信息的结构与格式
  2. 语义:即需要发出何种控制信息,完成何种动作以及做出何种相应
  3. 时序:即事件实现顺序的详细说明

协议(protocol)最终体现为在网络上传输的数据包的格式

2.知名协议的默认端口号

  之前我们说过,系统端口号范围是0~65535,其中:0 ~ 1023为知名端口号,这些端口预留给服务器端程序绑定广泛使用的应用层协议,比如;

  • 21端口:预留给SSH服务器绑定SSH协议
  • 22端口:预留给FTP服务器绑定FTP协议
  • 23端口:预留给Telnet服务器绑定Telnet协议
  • 80端口;预留给HTTP服务器绑定HTTP协议
  • 443端口:预留给HTTPS服务器绑定HTTPS协议

  以上只是说明0 ~ 1023范围的知名端口号用于绑定知名协议,但知名协议不一定会使用知名端口号来绑定,其他普通端口号也是有可能的

3.五元组

在TCP/IP协议中,用五元组来标识一个网络通信:

  1. 源IP:标识源主机
  2. 源端口号:标识源主机中该次通信发送数据的进程
  3. 目的IP:标识目的主机
  4. 目的端口号:标识目的主机中该次通信接收数据的进程
  5. 协议号:标识发送进程和接收进程双方约定的数据格式

(四)协议分层

1.概念

  由于我们传输数据需要约定各种标识,IP,协议,数据,物理传输方式,传输路径等等一系列的东西,如果放在一起就会非常复杂
  因此我们在进行网络通信时,将协议进行分成了好几层来分别约定规则

2.作用

  分层最大的好处就是,就是类似面向接口编程:定义好层与层之间的接口规范,让双方遵循这个规范来对接
  在代码中,类似于定义好一个接口,一方为接口的实现类(提供方,提供服务),一方为接口的使用类(使用方,使用服务):

  • 对于使用方来说,并不关心提供方是如何实现的,只需要使用接口即可
  • 对于提供方来说,利用封装的特性,隐藏了实现的细节,只需要开放接口即可

这样能更好的扩展和维护

3.OSI七层模型

OSI:即Open System Interconnection,开放系统互连

  • OSI 七层网络模型是一个逻辑上的定义和规范:把网络从逻辑上分为了7层;
  • OSI 七层模型是一种框架性的设计方法,其最主要的功能就是帮助不同类型的主机实现数据传输;
  • 它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整。通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯

OSI七层模型分为以下七层:
OSI七层协议

OSI 七层模型既复杂又不实用:所以 OSI 七层模型没有落地、实现
实际组建网络时,只是以 OSI 七层模型设计中的部分分层,也即是以下 TCP/IP 五层(或四层)模型来实现

4.TCP/IP协议五层(或四层)模型

  TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。
  TCP/IP通讯协议采用了5层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求

TCP/IP五层协议

  • 应用层:负责应用程序间沟通(传输的数据具体是干嘛的),如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。我们的网络编程主要就是针对应用层
  • 传输层:负责两台主机之间的数据传输(端到端之间的通信,只关注结果,不关注过程)。如传输控制协议 (TCP),能够确保数据可靠的从源主机发送到目标主机
  • 网络层:负责地址管理和路由选择(点到点之间的通信,规划路线)。例如在IP协议中,通过IP地址来标识一台主机,并通过路由表的方式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)工作在网络层
  • 数据链路层:负责设备之间的数据帧的传送和识别(相邻的两个设备之间的通信)。交换机(Switch)工作在数据链路层。
  • 物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞 线)、早期以太网采用的的同轴电缆(现在主要用于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层

物理层我们考虑的比较少。因此很多时候也可以称为 TCP/IP四层模型
参考资料
https://blog.csdn.net/superjunjin/article/details/7841099

5.网络设备分层

  • 对于一台主机,它的操作系统内核实现了从传输层到物理层的内容,也即是TCP/IP五层模型的下四层;
  • 对于一台路由器,它实现了从网络层到物理层,也即是TCP/IP五层模型的下三层;
  • 对于一台交换机,它实现了从数据链路层到物理层,也即是TCP/IP五层模型的下两层;
  • 对于集线器,它只实现了物理层

  这里说的是传统意义上的交换机和路由器,也称为二层交换机(工作在TCP/IP五层模型的下两层)、三层路由器(工作在TCP/IP五层模型的下三层)
  随着现在网络设备技术的不断发展,也出现了很多3层或4层交换机,4层路由器。我们以下说的网络设备都是传统意义上的交换机和路由器

6.封装和分用

  封装和分用是指不同的分层的协议之间,是如何相互配合的

  • 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)
  • 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装(Encapsulation)
  • 首部信息中包含了一些类似于首部有多长,载荷(payload)有多长,上层协议是什么等信息
  • 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理

封装:

  • 应用层的协议是程序猿自己约定的。应用层协议调用操作系统提供的API( 称为socket API),把应用层的数据,交给传输层(此时就进入了操作系统内核)
  • 传输层最典型的协议:UDP,TCP。以TCP为例,构造一个传输层的协议报文。TCP数据报:TCP报头(包括源端口和目的端口) + 数据载荷(Payload,也就是一个完整的应用层数据)。传输层在拿到应用层数据后会对数据进行封装,并拼接一个运输层协议报头。接下来传输层的数据报就会交给网络层

传输层封装

  • 网络层在拿到了传输层数据报之后,根据当前的网络层协议(例如IP),再次进行封装,把TCP数据报构造成IP数据报,还是加上一个协议报头。IP数据报 = IP协议报头(包括源IP和目的IP) + 载荷(完成的TCP/UDP数据报)。接下来网络层的数据报就会交给数据链路层

网络层封装

  • 数据链路层的拿到网络层数据报之后,根据当前使用的数据链路层协议,构造出一个数据链路层的数据报,典型的数据链路层协议,叫做“以太网”,把IP数据报构造成一个“以太网数据帧”。以太网数据帧 = 帧头 + IP数据报 + 帧尾。帧头中最重要的数据就是接下来要传给的设备的地址(指的是相邻节点的地址),随着数据往下一个设备转发,帧头中的地址,时刻在发生变化。帧尾中重要的是校验和。接下来,数据链路层的数据报就会交给物理层

数据链路层封装

  • 物理层做的工作,就是根据刚才的以太网数据帧(二进制01数据),把这里的01数据转成高低电平(或高低电频的电磁波),通过网线(光纤/无线)传输出去。下面数据就离开了当前主机。前往了下一个设备,下一个设备可能是路由器/交换机/其他设备…

整体的封装过程如下:
封装

分用

  • 主机B的网卡感受到一组高低电平,然后就会把这组高低电平解析为01数据,这一串01数据共同构成一个完整的以太网数据帧,然后交给数据链路层
  • 数据链路层拿到数据帧之后,对数据帧进行解析,把里面的IP数据报取出来,交给网络层
  • 网络层拿到IP数据报之后,解析,去掉IP数据报头,把里面的TCP数据报取出来,交给传输层
  • 传输层拿到TCP数据报,解析,去掉TCP数据报头,把里面的应用层数据报取出来,交给应用层
  • 应用层就会调用socket API,从内核中读取到应用层数据报,再按照应用层协议解析,根据解析结果决定该做什么

分用

  我们之前说过,两个主机之间进行网络通信,可能要经过多个设备。以路由器为例:在数据传输到路由器之后,路由器自身也实现了TCP/IP协议模型的下三层,即网络层到物理层,因此路由器在拿到以太网数据报之后同样会分用,在解析到IP数据报时,路由器就会知道源IP和目的IP,此时它就会重新规划得到最优传输路线,然后到数据链路层把帧头要传输的下一个设备的地址进行更新。而传统交换机和路由器类似,但是它只到数据链路层,即它只是更新以太网数据帧的帧头

网络通信中间过程

三、网络编程基础

  我们想要获取到网络中的各种数据资源,就需要通过网络编程来进行数据传输

(一)概念

  网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的
  当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程
  对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:

  • 进程A:编程来获取网络资源
  • 进程B:编程来提供网络资源

(二)网络编程中的基本概念

1. 发送端和接收端

在一次网络数据传输时:

  1. 发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
  2. 接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
  3. 收发端:发送端和接收端两端,也简称为收发端。

注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念

2. 请求和响应

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

  1. 第一次:请求数据的发送
  2. 第二次:响应数据的发送

3. 客户端和服务端

  1. 服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
  2. 客户端:获取服务的一方进程,称为客户端

客户端服务端网络通信过程:

  1. 客户端先发送请求到服务端
  2. 服务端根据请求数据,执行相应的业务处理
  3. 服务端返回响应:发送业务处理结果
  4. 客户端根据响应数据,展示处理结果(展示获取的资源,或提示保存资源的处理结果)

四、Socket套接字

(一)概念

  网络编程套接字,是操作系统给应用程序提供的一组API(叫做socket API)。Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,可以认为是应用层和传输层之间通信的桥梁。
  传输层最重要的协议有两种,TCP,UDP,socket API也有对应的两组。由于TCP和UDP差别比较大,因此这两组API差别也挺大的

(二)分类

Socket套接字主要针对传输层协议划分为三类:流套接字(TCP),数据报套接字(UDP),原始套接字(用于自定义传输层协议,读写内核没有处理的IP协议数据,只做了解)

  • TCP,即Transmission Control Protocol(传输控制协议),传输层协议
  • UDP,即User Datagram Protocol(用户数据报协议),传输层协议
TCPUDP
有连接无连接
可靠传输不可靠传输
面向字节流面向数据报
全双工全双工
  1. 有连接,AB主机之间必须建立了连接,才能交互数据
    无连接,AB主机之间不需要建立连接,直接就能发送数据
  2. 可靠传输,传输过程中,发送方知道接收方有没有收到数据(类似企业微信的已读)
    不可靠传输,传输过程中,发送方不知道接收方有没有收到数据
    注意:可靠不是保证数据100%发送过去,而是知道是否发送过去,也不能理解为“安全传输”
  3. 面向字节流,以字节为单位进行传输,类似文件IO中的字节流
    面向数据报,以数据报为单位进行传输(固定大小),一次发送/接收必须是一个完整的数据报
  4. 全双工,一条链路,双向通信(类似电话)
    半双工,一条链路,单向通信(类似广播)

五、UDP数据报套接字编程

两个核心类:DatagramSocketDatagramPacket

(一)DatagramSocket API

DatagramSocket是UDP Socket,用于发送和接收UDP数据报
  DatagramSocket创建一个实例就相当于创建了一个UDP版本的 socket 对象,代表着操作系统中的一个 socket 文件,而一个 socket 文件代表着网卡硬件设备
核心方法

方法方法说明
void receive(DatagramPacket p)从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报包,不会阻塞等待,直接发送
void close()关闭此数据报套接字

(二)DatagramPacket API

DatagramPacket是 UDP Socket发送和接收的数据报(大小自定),表示了一个UDP数据报,每次发送/接收数据,都是在传输一个 DatagramPacket 对象

(三)UDP服务器客户端案例

回显服务

即客户端发什么请求,服务器就返回什么请求
服务器代码示例

public class UdpEchoServer {
    //创建DatagramSocket对象
    private DatagramSocket socket = null;

    // 构造方法,传递端口号,必须手动指定端口号,因为客户端想要访问服务器的话
    // 必须事先知道服务器的IP和端口号
    //可能创建失败
    //1. 传递的端口号被占用
    //2. 该进程已经开了足够多的文件,无法再开辟新的文件
    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        //UDP 不需要建立连接,直接接收从客服端发来的数据即可
        while(true) {
            //1. 读取客户端发来的请求

            //要读取请求,就需要先创建一个DatagramPacket数据报
            //创建数据报时需要指定大小
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
            //此处同样可能会有异常,是我们熟知的IOException,另外此处如果收不到请求,会发生阻塞
            socket.receive(requestPacket);
            //将请求转为字符串类型
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
            //2. 根据请求计算响应
            String response = process(request);
            //3. 把响应发回客户端
            //把相应发回客户端同样需要一个数据报作为媒介
            //把字符串的响应按照字节数组写回数据报,需要指定字节长度
            //我们在发回客户端时,需要说明发给哪个客户端,因此我们需要从请求中获取到客户端的IP + 端口
            //因此创建数据报传参时应该传递IP + 端口
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length
                , requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //我们可以看一下发来请求的客户端的IP + 端口,请求内容和响应
            System.out.printf("[%s : %d] req: %s, resp: %s\n",
                    requestPacket.getAddress().toString(),requestPacket.getPort(), request, response);
        }
    }

    //由于我们是回显服务,因此直接返回请求即可
    private String process(String request) {
        return request;
    }

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

客户端代码示例:

public class UdpEchoClient {
    //创建一个socket实例
    private DatagramSocket socket = null;
    //指定服务器的IP和端口号
    private String serverIP;
    private int serverPort;

    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        this.socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    //构造方法,客户端直接可以让系统分配一个空闲端口即可
    public UdpEchoClient() throws SocketException {
        this.socket = new DatagramSocket();
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("启动服务器!");
        Scanner scanner = new Scanner(System.in);
        while(true) {
            // 1. 先从控制台读取用户输入的字符串
            System.out.print("请输入请求内容-> ");
            String request = scanner.nextLine();
            // 2. 把输入的字符串构造成一个UDP请求并发送
            // 需要指明把数据发给谁
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);

            socket.send(requestPacket);
            // 3. 从服务器读取响应数据,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
            // 4. 把响应结果显示到控制台上
            System.out.printf("req: %s resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

六、TCP流套接字编程

两个核心类:ServerSocketSocket

(一)ServerSocket API

ServerSocket创建TCP服务端Socket的API
核心方法

方法方法说明
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端 Socket 对象,并基于该 Socket 建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

  由上述方法中,我们可以看到accept()方法与UDP的方法非常不一样,原因就是TCP的特点是有连接,而UDP是无连接的,因此UDP的服务器只需要等待请求即可,而TCP在传输请求之前,应该先与客户端建立连接,然后才能进行网络通信

(二)Socket API

  Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
  不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据

核心方法

方法方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStreanm getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

(三)TCP服务器客户端案例

回显服务

即客户端发什么请求,服务器就返回什么请求
服务器代码示例

public class TcpEchoServer {
    //先创建一个TCP服务端的Socket的套接字
    private ServerSocket serverSocket = null;

    //构造方法与UDP类似
    //参数传递端口号
    public TcpEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            // TCP的服务器要想和客户端进行网络通信,必须先建立连接
            // 而accept()方法就是用来接收客户端请求连接的信息的
            // 接收请求之后,accept会返回一个Socket对象
            // 这个对象才是具体和对应的客户端进行通信的实例
            Socket clientSocket = serverSocket.accept();
            //处理连接
            //如果不用多线程,那么服务器一次只能处理一个请求,只能和一个客户端进行网络通信
            //因为processConnection只是一个方法,而方法内部在循环接收唯一的客户端的请求
            Thread t = new Thread(() -> processConnection(clientSocket));
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s : %d] 客户端建立连接\n",clientSocket.getInetAddress().toString(), clientSocket.getPort());
        //要处理连接,就要先获得请求的输入流和输出流对象
        try(InputStream inputStream = clientSocket.getInputStream()) {
            try(OutputStream outputStream = clientSocket.getOutputStream()) {
                //为了方便的得到请求转成的字符串,我们直接用Scanner来接收
                Scanner scanner = new Scanner(inputStream);
                //接着循环处理请求
                while(true) {
                    //当没有请求的时候,程序执行完毕
                    if(!scanner.hasNext()) {
                        System.out.printf("[%s : %d] 客户端断开连接\n",clientSocket.getInetAddress(), clientSocket.getPort());
                        break;
                    }
                    //获取请求
                    String request = scanner.next();
                    //处理请求
                    String respond = process(request);
                    //发送请求,为了发送的方便,我们使用PrintWriter类对outputStream进行封装
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(respond);
                    //刷新缓冲区,让客户端第一时间看到反馈
                    printWriter.flush();
                    System.out.printf("[%s : %d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, respond);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            //此处clientSocket必须释放,因为服务器和该客户端已经断开连接了
            //就不再让它占用资源
            try {
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

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

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

客户端代码示例:

public class TcpEchoClient {
    //需要先创建一个实例,来与服务器进行连接和网络通信
    private Socket socket = null;
    private String serverIP = null;
    private int serverPort = 0;

    //此时指定的IP和端口号是要连接的服务器的
    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        this.serverIP = serverIP;
        this.serverPort = serverPort;
        socket = new Socket(serverIP, serverPort);
    }

    //与服务器进行网络通信
    public void start() {
        System.out.println("与服务器连接成功");
        Scanner scanner = new Scanner(System.in);
        //我们需要将字符串写入socket的输入流,就需要先创建出来输入输出流实例
        try(InputStream inputStream = socket.getInputStream()) {
            try(OutputStream outputStream = socket.getOutputStream()) {
                while(true) {
                    //建立连接之后我们需要进行四步操作
                    //1. 从控制台读取用户输入的字符串
                    System.out.print("-> ");
                    String request = scanner.next();
                    //2. 用读取的字符串构造一个请求,然后发给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    //传一个请求就刷新一次,保证服务器能立即看到请求并响应
                    printWriter.flush();
                    //3. 接收服务器传过来的响应并解析
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    //4. 把服务器的响应打印到控制台
                    System.out.printf("req: %s, resp: %s\n", request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
        tcpEchoClient.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求索1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值