admin 管理员组

文章数量: 887021

一、网络发展的历史

1.中国的网络发展大概在2000年左右开始兴起,越来越多的能联网的游戏出现了,局域网联网,广域网联网.

局域网与广域网没有一个,明确的限制的,(中国的网络可以说是广域网也可以说是一个比较大的局域网)

2.在2007 年的时候乔布斯发明了第一代苹果智能机,智能手机登上历史舞台,标志着网络时代开始往"移动互联网时代"进军

3.2008年左右,安卓被发明了.

4.2011/2012年左右,小米以及华为等厂商发布了"亲民价"智能手机,进一步推动了智能机在全球的普及,很快智能手机取代了功能机,成为了如今我们手中的必不可缺的工具.

5.2014年左右掀起了全民互联网创业的浪潮.(美团和滴滴都是诞生于这个时代)

二、组件网络中所涉及的重要设备

交换机:组建局域网.

路由器:本质是就是将两个局域网连起来.

集线器:就是说把一根网线掰成两根来使用,它的功能由路由器和交换机都可以替代,所以现在已经几乎消失了.

解释一下:

如果用网线将电脑与电脑之间相连,就可以实现电脑之间的通信.

但如果我们这里有四台电脑呢?

我们就可以使用交换机进行电脑之间的相连.(这是就组成了局域网)

但如果这时我们有更多的电脑需要相连呢?

这时就需要用到路由器了.(而路由器可以实现跨局域网进行连接)

三、网络通信基础

    • IP地址:标识了网络上设备所在的位置.(就是外卖中的地址)
    • 端口号:标识了一个具体的应用程序.(就是外卖中填写的)
    • 协议:协议是网络编程中最最核心的概念.也就是约定.(商量好数据传输是根据怎样的格式进行传输的,有了这个约定才能让双方理解对方的含义)

比如说两个人想要对话,首先要使用一样的语言,这里的语言就是双方的"协议"

认识协议:

协议一般都很复杂(因为协议要完成的任务比较多),为了更好的设置和维护协议,我们一般把协议拆分成一些很小的协议(每个协议负责一部分功能),这是我们发现某些小的协议的功能都是一样的,我们再根据这些小的协议进行再分层,也就是"分类",功能差不多的放到同一个包里.

好处:

1)降低了学习成本和维护成本(就比如下面的这种情况,使用电话进行沟通的人不需要理解电话机之间拥有着某种协议,只需要理解人与人之间的"语言协议"即可)

2)可以灵活的针对里面的某一层协议进行替换.(如上面人与人之间改为英语也可进行通话)

当前的互联网世界的协议,主要有两种:

OSI七层网络模型(教科书上的模型,实际上没有)

TCP/IP五层(四层)网络模型(实际上的模型)这是OSI的简化实现模型

这里主要说一下TCP/IP五层(四层)网络模型:

越往下越接近硬件设备,越往上越接近用户(应用程序之类的)

物理层:

约定了网络通信中硬件设备的规模,比如网线的尺寸、材质、功能等.(确保了电脑与电脑之间硬件设备的一致)

传输层:

只考虑数据传输的起点和终点,不关心数据传输的过程.(端到端之间的传输)

也就是说在我们寄快递的时候买家和卖家只需要考虑快递的起点和终点,不需要考虑中间的运输过程一样.

网络层:

主要负责路径的规划,走哪条路比较划算.

也就是在寄快递的时候快递公司所要思考的工作.(比如我们要从北京到上海,该怎么走,走哪条路划算)

数据链路层:

主要负责相邻的两个节点之间,具体该怎么进行传输.(比如说北京到上海走空运更加省钱之类的)

在实际的工作中,数据是通过光纤传输还是wifi传输,通过哪个网口出比较快,都是数据链路层负责的内容.

网络分层这里相当于上层协议要调用下层协议,下层协议给上层协议提供服务.

应用层:

也就是应用程序,描述了传输的数据该怎样被使用.

网络数据传输的基本流程(站在协议分层的背景下来理解):

以QQ为例,A给B发送一个hello:

首先用户在输入框中输入一个hello这个字符串,qq这个应用程序,就把这个字符串构造成一个应用层数据报.

1.假设一个应用层协议的格式(程序员可以自己设置):
发送方qq号;发送时间;接收方qq号;消息内容.

应用层数据报本质上就是一个携带了一些信息的字符串.

程序要调用操作系统的api,把这个应用层数据,交给传输层

2.传输层(进入系统内核了)

在传输层中,就要把上述应用程序,构造成传输层数据报,其中最有名的协议当属UDP/TCP了,比如此处要使用UDP,就需要构造出UDP报头.

UDP报头也是一个特定格式的字符串,此处就像字符串拼接,将两部分拼接在一起.

UDP报头中包含源端口和目标端口

3.网络层(最有名的就是IP协议了,IP协议要基于上述数据,打包成一个IP数据报)

IP报头中包含源IP与目的IP

所以说在数据的传输过程中,我们需要知道源IP、源端口、目的IP、目的端口加上协议类型,他们被成为数据传输的五元组

4.数据链路层(其中最有名的是"以太网",基于上述数据,还要打包成一个"以太网数据帧")

5.物理层

把上述二进制数据转换成电信号/光信号,发送出去.

从应用层到物理层,层层加码,上述过程我们称之为封装.

接收方的工作就是按照顺序在每一层中将报头去除并提取出里面的信息,每层只处理对应的信息,以达到相互独立,互不影响的目的最后将hello显示在qq的显示界面,这个过程我们称之为"分用".

总结:

上层协议要调用下层协议(上层将文件交给下层继续封装),下层协议给上层协议提供服务(下层解析好数据交给上层),这几层协议之间存在着严格的层级关系(不能跨曾交互)

在这里发送方和接受方使用的协议都得是一致的才可进行信息的传送.

四、网络编程

这里的核心:Socket API(套接字),这是操作系统给应用程序提供的网络编程的api

传输层里提供了两个最核心的协议:UDP/TCP,因此socket api也提供了两种风格UDP/TCP.

    • 简单认识一下UDP和TCP

UDP:无连接、不可靠、面向数据报、全双工.

TCP:有连接、可靠传输、面向字节流、全双工.

解释一下上述名词:

连接:有无连接就如打电话和发短信,如果你要打电话传输信息,需要建立连接,而且你可以明确的知道对方是否收到你传输的信息,而发短信不行,所以说打电话是有连接,而发短信是无连接.

可靠传输/不可靠传输:因为网络环境是复杂的,所以我们无法保证数据100%发过去,但是可靠传输就像打电话,可以知道信息是否成功的发送了过去,而不可靠传输(发短信)不行.

面向字节流/数据报:这个就是传输的单位.面向字节流就是跟文件IO一样是"流式"读写,而面向数据报是以数据报为单位进行读写(一次读写可能有若干个字节,是带有一定的格式的).

全双工/半双工:这个就是指信息传输的方向.半双工就是信息只能按照一个方向进行传输(比如水管),全双工就是指信息可以双向进行通信(我们的互联网就是这样).

    • 基于UDP来编写一个简单的客户端服务器的网络通信程序

客户端服务器程序主要基于这两个类:
1.
  • 我们使用这个类来描述一个socket对象,在操作系统中,是把这个socket当做一个文件来处理的,相当于是文件描述符表上的一项.但是,普通的文件对应的硬件设备是硬盘,而socket文件对应的硬件设备是网卡(网络编程遵循一切皆文件原则).

  • 一个socket文件可以和一台主机进行通信,而想要和多个主机进行通信就需要多个对象.

这是这个类的构造方法:
  • 需要提供这个进程的端口号.

  • 一台系统上存在多个端口号,端口号是应用程序进入操作系统的"入口",一个进程拥有一个或多个端口号,但是,这里相当于是讲进程中的socket对象和端口号建立联系,然后程序直接调用socket对象.

  • 没有指定端口号就是随机端口号.

另外,操作系统还给我们提供了两个api:

显而易见,一个是接受,一个是发送,但他俩都需要DatagramPacket

这个就是表示UDP中传输的一个报文,可以理解为传输的基本单位.构造好一个DatagramPacket之后直接发送就可以了.

这里的DatagramPacket相当于是一个空的文件,但是在receive中会自动填充,从而产生结果所以说它也是一个输出型参数.

释放资源,释放文件描述符表中的表项,所以说socket用完之后要记得关闭.

2.

这个类上面说过是socket传输时的报文,为了构造这个对象我们需要指定一些数据进去

这里就提供了一些方法:

第一个方法就是在DatagramPacket中指定了一个缓冲区的类型以及长度.

而第二个方法就是在第一个的基础上加上了缓冲区的IP和端口号(port).

这三个方法相当于是将报文中的数据取出来

知道了这些我们就可以编写一个最简单的UDP版本的客户端服务器程序(回显服务器):

先是UDP版本的服务器代码:

回显服务器:就是发什么收什么,这里我们主要是学习数据传输的过程,而不是服务器功能(没有实际的作用,只是展示了socket 的基本用法).

首先我们需要先创建一个socket对象.

这里我们无法直接操作网卡,所以构造出了一个socket对象间接操作网卡,所以说只要我们进行网络编程就必须需要socket对象.

接下来,实例化socket对象:

注意:这里我们编写的是服务器部分,而服务器必须需要一个端口号(上面讲过这个构造方法),在网络信息传输的过程中,服务器永远处于被动接收信息的一方,如果没有端口好,会让客户端无法获取到客户端的准确位置.(就像你回家,如果没有几单元几号楼和门牌号,你就不知道你家在哪也就没法回家一样)

接下来写服务器的运行部分:

这里要想明白一件事:我们的服务器需要对应多个客户端,所以接收信息的操作需要在循环中进行.

这里我们需要使用文件接收的基本单位:DatagramPacket进行文件的接收,所以我们首先要构造一个空的DatagramPacket对象.

这里的DatagramPacket和receive上面也讲过.再简单说一下,其实就是构造了一个指定大小的碗(DatagramPacket),然后递给食堂阿姨(receive),让她往里装东西(网卡接收的数据).

这是我们用DatagramPacket接收到的数据都是DatagramPacket类的,难以处理,为了方便处理,我们需要把里面的内容取出来,装入String的盘子中.

注意:这里我们的DatagramPacket中装的是4096大小的byte数组,但实际上可能根本没有填满,这时我们只需要构造requestPacket.getLength()大小的数组即可.

之后我们要完成根据请求返回响应的功能:(回显功能)

之后我们需要把相应写回客户端:这里我们照样也需要一个DatagramPacket对象来盛放相应的字节数组.

在这里需要对比一下response.length()与response.getBytes().length,到底用哪个呢?

结论:使用response.getBytes().length,怎么接收的再以相同格式送出去,所以要送出去需要送出去字节,而不是String类型的字符.

字节和字符是不同的.DatagramPacket只认字节,不认字符.

这里我们还需要确定一个事,因为服务器要处理复数个客户端,为了我让返回的相应去错地方,这里我们的DatagramPacket中还需要指定返回地址:

来的时候是通过requestPacket 的客户端和端口号进来的,返回我们也需要使用它的getSocketAddress()出去.

也就是怎么来的,怎么走.

最后,再加上服务器的打印日志:

这时候,我们的UDP版本的服务器代码就完成了.

package network;

import java.io.IOException;
import java.DatagramPacket;
import java.DatagramSocket;
import java.SocketException;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-02
 * Time: 17:29
 */
//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){
            DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(requestPacket);
            String request=new String(requestPacket.getData(),0,requestPacket.getLength());
            String response=process(request);
            DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length
            ,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            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 SocketException {
        UdpEchoServer server=new UdpEchoServer(9090);
        server.start();
    }
}

服务器需要不断地去处理客户端的请求,所以说服务器需要24小时运行,时刻准备着.

再加上这个receive()方法的阻塞功能,将没有客户端请求的时间都进入阻塞.

这里还有一个问题,就是如果客户端请求的过于密集了,这时服务器处理不过来了,我们该怎么办呢?

回答:多线程(看上一个博客),或者多加机器(多开几个服务器,也就是分布式).

接下来,就是UDP版本的客户端代码了:

首先,依然是构造一个DatagramSocket对象:

但是这里最大的区别是,这个socket对象不需要显式绑定一个端口.

这里需要说一下五元组:原IP与目的IP都是本主机所以不需要考虑,而目的端口也就是上面讲到的服务器端口需要手动设置,但是原端口也就是客户端端口需要随机挑选一个空闲的端口,而谁也不知道客户端主机上的运行着什么程序,占用着哪些端口,所以这里我们采取系统给予的方式解决这个问题.

现在要确定:一次通信我们需要知道两个IP(原IP,目的IP)和两个端口(原端口,目的端口)

所以这些在客户端中都需要我们手动指定.

接下来就是启动客户端:

首先是读取控制台的数据,判断控制台的数据是不是exit如果是退出.

接下来是将这个请求构造成UDP格式,并发送:

注意构造这个socket的时候需要把serverIp和serverPort都传过来,但是此处我们需要的IP地址是一个32位的整数形式,而上述的serverIP是一个字符串形式,所以我们需要使用InetAddress.getByName()来转换

咱们看到的IP地址,如:127.0.0.1 =>是一个32位的整数,就相当于两个 . 之间是四个bit位(给计算机看的)

接下来将包好的requestPacket发送过去:

然后构造一个空的DatagramPacket类型的相应数组进行response内容的填充

然后我们再将收到的相应内容转换成字符串形式,然后进行打印就行了:

这是客户端代码就完成了:

package network;

import java.io.IOException;
import java.*;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-02
 * Time: 17:31
 */
public class UdpEchoClient {
    private DatagramSocket socket=null;
    private String serverIp=null;
    private int serverPort=0;
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        socket=new DatagramSocket();
        this.serverIp=serverIp;
        this.serverPort=serverPort;
    }
    public void start() throws IOException {
        System.out.println("客户端启动!!!");
        Scanner scanner=new Scanner(System.in);
        while(true){
            System.out.print(">");
            String request=scanner.next();
            if(request.equals("exit")){
                System.out.println("goodbye!!!");
                break;
            }
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length
            , InetAddress.getByName(serverIp),serverPort);
            socket.send(requestPacket);
            DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);
            String response =new String(responsePacket.getData(),0,requestPacket.getLength());
            System.out.println(response);
        }
    }
    public static void main(String[] args) throws IOException {
        UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

下面总结一个客户端和服务器的这个信息的"流动"过程:

首先是服务器和客户端通过五元组建立连接,然后是客户端通过socket.send()发送信息,然后是服务器通过socket.receive()接收信息,将其转化为字符串形式并加以处理,然后再由socket.send()发送处理并包装好的DatagramPacket信息,客户端再由socket.receive()接收,最后打印.

上图是服务器和客户端执行的顺序.

现在,使用一下服务器与客户端代码:(记住要先启动服务器再启动客户端)

现在输入一个hello试试:

这样这个程序就完成了!!!

但如果我们想要服务器同时处理多个客户端呢?

点击这个:

勾选这个:

就可以实现在控制台多客户端运行了!!!

下面我们在原有代码的基础上进行更改,新添一个"插词典"的功能:

与DictServer相比,EchoServer基本上是和DictServer一致的,所以在这里我们就直接继承EchoServer就可以了:

主要是根据请求计算响应这个过程不太一样!!!

所以在这里,我们直接重写process方法:

利用HashMap进行这个程序的编写, 设置键值对已达到词典效果:

这样程序就完成了:

package network;

import java.io.IOException;
import java.SocketException;
import java.util.HashMap;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-03
 * Time: 18:59
 */
public class UdpDictServer extends UdpEchoServer{
    private Map<String ,String> dict=new HashMap<>();
    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("cat","小猫");
        dict.put("dog","小狗");
        dict.put("elephant","大象");
    }
    @Override
    public String process(String request){
        return dict.getOrDefault(request,"当前单词未在词典中收录!!!");
    }

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

程序也可以正常运行!!!

3.基于TCP的socket api 的使用情况(回显版本)

TCP提供的api主要是两个类:
  1. ServerSocket 专门给服务器使用的api

  1. Socket即可以给服务器使用也可以给客户端使用.

注意:TCP不需要一个类似DatagramPacket的类表示表示数据报用来传输,因为TCP是以"流"的形式进行传输的.

TCP中的各种常用类:

accept()类似接电话,就是建立连接.接了电话之后会返回一个Socket对象,通过这个Socket对象实现客户端和服务器之间的沟通.

之后是这两个:

进一步通过Socket对象,获取到内部的流对象,借助流对象来实现信息的 发送/接收.

下面完成代码:

首先还是实例化对象,并填写端口:

然后是设立start()方法启动服务器,编写while循环:

接下来使服务器和客户端建立连接:

注意:这里的accept是"建立连接",想要建立连接,必须有客户端和它连接,客户端指定IP和端口号可以和accept建立连接,在没有连接时,它是阻塞等待的.

使用Socket进行accept()返回值的接收:

注意:在TCP版本中有两种Socket对象一种是:

ServerSocket;

一种是:

Socket

对这两种Socket对象加以理解,是我们在理解TCP版本的服务器编程中至关重要的一部!!!

假设有个浴场:

ServerSocket相当于是浴场外堂的伙计,他只负责将不认识路的你领入内堂,并将你交给内堂伙计socket,而socket才是负责你服务你泡澡、搓澡、喝茶的人.他们两个各司其职,各自负责各自的位置.

接下来,我们需要一个类专门处理clientSocket的类:

接下来,我们要获取clientSocket的字节流对象:(文件IO博客有讲)

此处我们依然需要一个循环处理的流对象:

接下来我们读取请求:这里我们可以使用Scanner()包裹请求inputStream进行"流式"读取,然后设置一个条件,是循环结束(客户端下线).

这里的条件!scanner.hasNext的意思就是如果读取的内容没有下一个,循环就结束.

接下来我们将读到的信息转化为字符串形式,再传入process方法中的到服务器响应的结果:

之后我们想直接利用outputStream.write()写入响应,但发现write()没有写入字符串的功能,所以这里我们还需要进行一部字符流的转换:

这里的printwrite相当于是输入流对象的转换类,也就是要将输入流发送至客户端需要先采用printWrite进行包装

这里我们利用println进行写入,是为了给写入提供一个换行符,让它能够更好的读取.

然后进行flush()刷新缓冲区.

最后进行服务器日志的撰写:

这时的代码已经完成了十四之八九了,但还差着一步:

这里的clientSocket,任意一个客户端连上来都需要创建一个Socket对象,每创建一个Socket对象都要占用一个文件描述符表,且不会及时的自动销毁,但是上面我们将过文件描述符表是有限度的,所以在使用完成Socket之后要及时释放.

之前的socket是随着其存在的生命周期进行工作的,所以在其工作结束的一瞬间就会自动释放,不会影响程序运行,但这个Socket是存在到程序结束(而且服务器不会轻易结束),所以要手动释放.

这样服务器部分的代码就完成了!!!

package network;

import java.io.*;
import java.ServerSocket;
import java.Socket;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-03
 * Time: 19:44
 */
public class TcpEchoServer {
    private ServerSocket serversocket=null;
    public TcpEchoServer(int port) throws IOException {
        serversocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器!!!");
        while(true){
            Socket clientSocket=serversocket.accept();
            processConnect(clientSocket);
        }
    }

    private void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!!!",clientSocket.getInetAddress().toString()
                ,clientSocket.getPort());
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            while(true){
                Scanner scanner=new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线!!!",clientSocket.getInetAddress().toString()
                    ,clientSocket.getPort());
                    break;
                }
                String request=scanner.next();
                String response=process(request);
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s ;resp: %s\n",clientSocket.getInetAddress().toString()
                ,clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }

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

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

接下来是TCP版本的客户端代码:

依然是输入服务器的IP和端口号:

将构造方法中传入服务器IP和端口号相当于输入电话号码,是必须存在的行为!!!

这里的Socket可以直接识别"点分十进制",不需要再转换了!!!

new这个对象相当于建立了连接.

注意:这里的socket与服务器的socket不是同一个;可以把他们想成电话的两端.

理论上来说,再客户端和服务器建立连接之后,两个Socket分别代表双方的听筒部分进行信息的传输,在客户端和服务器部分分别使用getInputStream和getOutputStream进行文件的自由读取与写入:(如下图)

接下来就是编写start方法,读取控台的内容:

接下来同之前服务器的构造相同采用try包裹循环的方式引入写入输出流:

这是使用字符串包装的形式将读取到的内容进行包装,发送给服务器进行处理:(跟服务器是一样)

接下来进行服务器响应的读取和在控制台进行打印:

这是TCP版本的客户端代码就完成了!

package network;

import java.io.*;
import java.ServerSocket;
import java.Socket;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-03
 * Time: 20:49
 */
public class TcpEchoClient {
    private Socket socket=null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket=new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("客户端启动!!!");
        Scanner scanner=new Scanner(System.in);
        try(InputStream inputStream=socket.getInputStream();
            OutputStream outputStream=socket.getOutputStream()) {
            while (true) {
                System.out.print(">");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("客户端退出!!!");
                    break;
                }

                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();

                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                System.out.println(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();
    }
}

这样程序就成功了!!!

这里问一个问题:在TCP的交流中都是使用printWrite.println()进行数据的传输的,但如果我们只是使用printWrite.print()能够达到现在的效果吗? '\n'起到了什么作用呢?

答案:不能!因为TCP是采取字节流的形式进行文件的传输,所以它是没有一个准确的读取截止标志的,所以说它将'\n'作为了读取截止的标志,而如果没有'\n',程序就将永远出入读取的过程中.

这里还有一个问题:就是服务器无法同时连接两个客户端!!!

我们发现,服务器只能识别并响应其中一个客户端!相当于在客户端1连接之后,服务器就"占线"了,当客户端1结束之后,客户端2就能正常工作了!

但是一个服务器服务多个客户端是我们的常识,为了解决此问题,我们要是用之前学习的:多线程进行操作.

我们只要微调服务器代码:

我们发现现在就可以同时处理多个客户端了:

但是呢,如果此时有大量客户端创建和销毁,会严重影响程序运行的效率,为了提高效率,我们应当采用线程池进行操作:

这里应当使用这个随申请线程数变化的线程池,而不是那个FixThreadPool(固定线程池)

这样就可以了!!!

程序也可以正确运行!!!

package network;

import java.io.*;
import java.ServerSocket;
import java.Socket;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: 86139
 * Date: 2023-02-03
 * Time: 19:44
 */
public class TcpEchoServer {
    private ServerSocket serversocket=null;
    public TcpEchoServer(int port) throws IOException {
        serversocket=new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器!!!");
        ExecutorService threadPool= Executors.newCachedThreadPool();
        while(true){
            Socket clientSocket=serversocket.accept();
            //此处我们使用多线程来处理
//            Thread thread=new Thread(() ->{
//                try {
//                    processConnect(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();
            threadPool.submit(()->{
                try {
                    processConnect(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
    }

    private void processConnect(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!!!",clientSocket.getInetAddress().toString()
                ,clientSocket.getPort());
        try(InputStream inputStream=clientSocket.getInputStream();
            OutputStream outputStream=clientSocket.getOutputStream()){
            while(true){
                Scanner scanner=new Scanner(inputStream);
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线!!!",clientSocket.getInetAddress().toString()
                    ,clientSocket.getPort());
                    break;
                }
                String request=scanner.next();
                String response=process(request);
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s ;resp: %s\n",clientSocket.getInetAddress().toString()
                ,clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            clientSocket.close();
        }
    }

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

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

TCP在建立连接的时候有两种表现形式:

  1. 短连接:客户端每次给服务器发送信息时,先建立连接,读取响应,再断开连接,等下次再发送时重新建立连接.

  1. 长连接:客户端建立连接之后,不断开连接,当经历多轮之后,发现短时间不会再进行信息的传送后,再断开连接.

上述TCP使用的是长连接的方式,

如果想要实现短连接的方式,可以直接把上述代码的客户端和服务器中的while(true)去掉既可以了.

4.C10K问题/C10M问题

就是一个线程处理10K(1w)或者10M(1kw)个客户端的解决办法:

IO多路复用/IO多路转接

也就相当于一个人接多个电话

原理:接电话不是每时每刻都在说话,说话是有停顿的,只要在停顿之时去回答另外一个电话的问题就好了!!!

这得利用到操作系统内核中的一些原生api

比如:select、poll、epoll

本文标签: 客户端 网络编程 服务器