网络编程 UDP 和 TCP

网络编程

网络编程的基本概念

什么是网络编程

在生活中是离不开网络的,我们经常从网络上获取一些资源(像通过浏览器浏览一些视频网站,从里面获取一些视频资源),这个过程都是通过网络编程来实现进行数据传输

网络编程 就是网络上的主机,通过不同的进程 ,进行网络传输数据(以网络编程实现的网络通信 )

只要是不同进程就行,因此即使是同一个主机,不同进程也可以进行网络编程

进程A:获取网络资源

进程B:提供网络资源

基本概念

1.接收端和发送端

发送端 :数据的发送方进程,称为发送端,此主机可以称为源主机
接收端 :数据的接收方进程,称为接收端,此主机可以称为目的主机
收发端 :发送端和接收端两端

  1. 请求和响应

当进程A可以请求数据发送 ,进程B就响应数据的发送

3.客户端和服务器

客户端 :获取服务一方的进程
服务器:提供服务一方的进程,可以对外提供服务

Socket套接字

概念

Socket套接字,是系统提供用于网络通信的技术,基于这个Socket编程网络程序开放就是网络编程

分类

1.流套接字 :TCP协议,即Transmission Control Protocol(传输控制协议)

特点:有连接、可靠传输、面向字节流、全双工、有接收和发送缓冲区、大小无限

2.数据报套接字 :UDP协议,User Datagram Protocol(用户数据报协议)

特点:无连接、不可靠传输、面向数据报、全双工、有接收缓冲区无发送缓冲区、大小受限一次最多传输64KB

3.原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据

1.有连接/无连接 :通信双方是否互相保存对方的核心信息(IP和端口号)

TCP就保存了,在结束的时候就释放了,UDP没有保存 ,但是传输信息中也是有相关信息的

2.可靠传输/不可以靠传输可靠传输就是丢包的概率 ,像TCP可靠传输,其丢包的概率比较低,UDP不可靠传输,不关心丢不丢包

3.面向字节流和数据报 :面向字节流,读写文件比较方便可以使用InputStream和OutputStream ;面向数据报:每次读取只可以以一个UDP为单位进行

4.全双工和半双工:全双工:可以双向传输,半双工:只可以单向传输


UDP数据报套接字编程

Java提供的一些API介绍
DatagramSocket

方法 说明
DatagramSocket() 创建一个UDP数据报套接字Socket,绑定本机任意一个端口号
DatagramSocket(int port) 创建一个UDP数据报套接字Socket,绑定到指定port端口号

端口号0~65535,但是 0 - 1023其有特殊含义不可以选择

这里一般情况下,不带参数的都用于客户端,一个服务器可能对应多个客户端,但是这些端口号不可以相同 ,如果我们自己手动创建可能会出现相同,因此让操作系统任意分配,这样可以有效防止端口号相同从而导致的冲突,手动指定端口号,带参数的构造方法一般由于服务器

方法 说明
void reveive(DatagramPacket p) 从p套接字接收数据报
void send(DatagramPacket p) 从套接字发送数据报
void close() 关闭数据报套接字

这里的receive方法,如果没有接收到数据报,其就会进行阻塞等待

而send不会阻塞等待,直接发送
DatagramPacket

方法 说明
DatagramPacket(byte buf[], int offset, int length) 构造一个DatagramPacket对象来接收数据报,保存在buf数组中,接收长度为length
DatagramPacket(byte buf[], int offset, int length, SocketAddress address) 同理,address是目的主机的IP和端口号
方法 说明
InetAddress getAddress() 从接收数据报中,获取发送端的IP地址,从发送数据报中,获取接收端IP地址
int getPort() 同理,用于获取端口号
byte[] getData() 获取数据报中的数据

InetSocketAddress

因为上面的DatagramPacket有一个参数是SocketAddress,这里的这个是其子类

方法 说明
InetSocketAddress(InetAddress addr, int port) 创建一个socket地址,包含地址和端口号

回显实例

服务器

1.此时的构造函数,根据自己提供的端口号进行实例化DatagramSocket 对象

2.启动过程
获取客户端请求

先创建一个DatagramPacket用于存放请求,并且要先申请好空间

并且使用receive接收请求,此方法中的参数是输出型参数,请求的内容会放入到requestPacket对象上
计算响应

此处是回显服务器,不做任何处理,将请求方发送信息发回去即可
将请求的结果返回给客户端

此处要将计算响应返回的字符串,转换成DatagramPacket返回给服务器

并且这里不仅要将数据放入进去,也要将发送请求的客户端对应的IP和端口号放入进去,这样才可以找到客户端

java 复制代码
//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("server start");

        while (true){
            //1.先获取请求,此时requestPacket是一个输出型参数
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);
            //将二进制转换成字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.计算响应(这里是回显服务器,这里没有计算过程)
            String response = process(request);
            //3.将请求返回给客户端,并且这里要传入客户端的地址
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志
            System.out.printf("[%s : %d],request:%s,response : %s",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
         System.out.println();
    }
    //此处的计算响应直接返回即可
    public  String process(String request){
        return request;
    }

    public static void main(String[] args) throws IOException {
        //这里的端口号不可以重复
        UdpEchoServer server = new UdpEchoServer(9900);
        server.start();
    }
}

客户端

构造方法 ,这里客户端并不需要指定端口号,让系统自动分配即可,因为这里客户端比较多,出现重复概率较高,让系统自动分配恰好解决了这一痛点

虽然我们不需要指定端口号,但是这个我们要确定客户端的IP和端口号,因为这样服务器进行接收数据时候才可以通过客户端的IP和端口号,将数据返回给客户端

启动客户端

1.先进行输入

2.将输入字符串变成一个DatagramPacket对象传给服务器 ,虽然UDP并不需要保存对方的核心信息(IP和端口号),但是也要知道这些信息 才可以找到对应的IP和端口号

3.接收服务器返回的数据

依旧使用一个DatagramPacket对象进行接收,并且这里receive参数是输出型参数

java 复制代码
//客户端
public class UdpEchoClient {
    //创建socket对象
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    private UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        socket = new DatagramSocket();//客户端不需要指定端口号,系统自动分配
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
    public void start() throws IOException {
        System.out.println("client start");

        Scanner scanner = new Scanner(System.in);

        //通过客户端将这个发送给服务器
        while (true){
            //1.用户进行输入
            System.out.print("->");
            String request = scanner.next();
            if(request.equals("exit")){
                break;
            }
            //2.将用户写入字符串构造成UDP数据报,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length,
                    InetAddress.getByName(this.serverIp),//将这个Ip转换成InetAddress
                    this.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());

            System.out.println(response);
        }

    }

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

客户端和服务器结合使用

exit结束客户端

这里可以一个服务器对应多个客户端

服务器

三个不同的客户端


这里可以将这个服务器修改一下,让其变成一个查询字典的功能

此时只需要一个新的类来继承上面的UdpEchoServer这个服务器即可 ,我们这里只有处理过程变了而已,所以继承重写一下process方法即可

java 复制代码
public class UdpDictServer extends UdpEchoServer{
    private Map<String ,String> map = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        //这里直接将对应关系建立起来即可
        map.put("hello","你好");
        map.put("world", "世界");
        map.put("cat", "小猫");
        map.put("dog", "小狗");
        map.put("pig", "小猪");
        map.put("like","喜欢");
    }

    @Override
    public String process(String request) {
        return map.getOrDefault(request,"没有找到这个单词");
    }

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

直接使用一个哈希表,将其对应关系单词和中文对应关系即可

TCP流套接字编程

ServerSocket

这个是创建TCP服务器端Socket的API
构造方法

方法 说明
ServerSocket(int port) 创建一个服务端流套接字Socket,绑定port端口号
方法 说明
Socket accept() 接收客户端连接,有客户端连接就,返回一个Socket对象,基于这个与客户端进行连接,否则阻塞等待
void close() 关闭此套接字

因为其创建对象也会占用空间,因此结束所有操作,要将这个数据给释放,否则会导致内存泄漏

Socket

Socket是客户端Socket,其是在双方进行联系的时候,保存对端的信息及其接受发数据

方法 说明
Socket(String host,int port) 创建一个客户端流套接字,与对应IP和端口号进建立连接

方法

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

回显实例

TCP服务器

1.构造函数同理使用,需要自己手动指定端口号

启动服务器

1.这里需要使用Socket流套接字对象来接收其请求

2.计算响应(这里是回显服务器,所有直接将其接收的数据返回即可)

3.返回

这里的ServerSocket负责接收数据,但是处理数据是通过Socket

这里ServerSocket就像酒店前台,在前台开了房间,而Socket就像酒店服务员将你带到你所在的房间

这里Socket对象是一个文件,因此可以使用这些关于文件的操作,进行读取和写入

java 复制代码
public class TcpEchoServer {
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);

    }

    //如果线程较多的话,其频繁的创建和销毁也是比较浪费时间的,因此可以使用线程池
    ExecutorService executorService = Executors.newCachedThreadPool();
    public void start() throws IOException {
        System.out.println("server start");
        while (true){
            //使用accept进行连接,并且会返回Socket对象
            Socket socket = serverSocket.accept();
            //通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
            processConnection(socket);
            //因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
            //但是使用多线程多了,其销毁和创建也需要时间
//            Thread thread = new Thread(() ->{
//               processConnection(socket);
//            });
//            thread.start();
            executorService.submit(() ->{
               processConnection(socket);
            });
        }
    }
    public void processConnection(Socket socket){
        System.out.printf("[%s:%d服务器上线",socket.getInetAddress(),socket.getPort());
        System.out.println();
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){

            Scanner scanner = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            while (true){
                //1.读取并进行解析
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d服务器下线",socket.getInetAddress(),socket.getPort());
                    break;
                }

                //2.根据请求计算响应
                String request = scanner.next();
                String response = process(request);
                //3.返回
                writer.println(response);
                writer.flush();

                System.out.printf("[%s:%d] req: %s, resp: %s\n", socket.getInetAddress(), socket.getPort(),
                        request, response);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            //这里完成以后要进行释放内存空间
            try {
                socket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }
    //直接返回即可
    public String process(String request){
        return request;
    }

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

客户端

构造方法,此时这里需要指定IP和端口号

启动客户端

这里依旧使用Scanner和writer分别进行发送和读取数据

java 复制代码
public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        //因为其是连接的,所以需要服务器Ip 和 端口
        socket = new Socket(serverIp, serverPort);
        //socket.connect(new InetSocketAddress((InetAddress.getByName(serverIp)),serverPort));

    }
    public void start(){
        System.out.println("client start");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){

            Scanner scanner = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            while (true){
                //1.从控制台输入
                System.out.print("->");
                String request = scanner.next();
                //2.将请求发送给服务器
                writer.println(request);
                writer.flush();//去除缓冲区
                //3.接收
                String response = scannerNetwork.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",9900);
        tcpEchoClient.start();
    }
}


虽然这样可以正常使用这个,但是如果换成多个客户端,这里会出现问题




这里就会出现这样的问题,只有一个服务器在操作,其他服务器都没有正常工作

引入多线程

因为上面无法连接多个客户端,因此这里我们可以将其设置成多多线程

java 复制代码
    public void start() throws IOException {
        System.out.println("server start");
        while (true){
            //使用accept进行连接,并且会返回Socket对象
            Socket socket = serverSocket.accept();
            //通过这个方法进入处理连接过程,并且一旦调用就不会调用accept
           // processConnection(socket);
            //因为这里会进行等待,导致这里只可以有一个客户端进行操作,因此这里可以使用多线程
            //但是使用多线程多了,其销毁和创建也需要时间
            Thread thread = new Thread(() ->{
               processConnection(socket);
            });
           thread.start();

        }
    }



引入线程池

但是当线程变多了,其创建和销毁也会比较浪费时间,因此这里就可以使用线程池

java 复制代码
    ExecutorService executorService = Executors.newCachedThreadPool();
    public void start() throws IOException {
        System.out.println("server start");
        while (true){
            //使用accept进行连接,并且会返回Socket对象
            Socket socket = serverSocket.accept();
            executorService.submit(() ->{
               processConnection(socket);
            });

其实可以进一步优化为IO多路复用,就是一个线程对应多个客户端,当某一个客户端需要处理时候,这个客户端对应的socket才会过来处理

细节处理

内存泄漏问题

在其服务器中会不断的创建socket,因此当这个对象不实用的时候,需要进行释放资源

缓冲区问题

这里的writer里面内置了缓冲区,为了避免重复的写入数据,其达到一定程度,当其缓冲区满了,其才是真正的写入,因此这里使用flush进行冲刷缓冲区,这样就不满了才是真正的写入

隐藏条件

TCP中,其请求和响应都是以\n进行结尾

当然这个也可以像UDP那样写一个查字典功能

java 复制代码
public class TcpDIctServer extends TcpEchoServer{
    private Map<String,String > map = new HashMap<>();


    public TcpDIctServer(int port) throws IOException {
        super(port);
        //这里直接将对应关系建立起来即可
        map.put("hello","你好");
        map.put("world", "世界");
        map.put("cat", "小猫");
        map.put("dog", "小狗");
        map.put("pig", "小猪");
    }

    @Override
    public String process(String request) {
        return map.getOrDefault(request,"没找到这个单词");
    }

    public static void main(String[] args) throws IOException {
        TcpDIctServer tcpDIctServer = new TcpDIctServer(9900);
        tcpDIctServer.start();
    }
}



相关推荐
凯子坚持 c1 小时前
Doubao-Seed-Code模型深度剖析:Agentic Coding在Obsidian插件开发中的应用实践
网络·人工智能
小尧嵌入式1 小时前
基于HAL库实现ETH以太网
网络·arm开发·stm32·单片机·嵌入式硬件
元气满满-樱1 小时前
思科:路由条目优化实验
网络·智能路由器
tan180°1 小时前
Linux网络IP(下)(16)
linux·网络·后端·tcp/ip
视觉震撼2 小时前
本地机器远程连接配置与文件传输可行性检测工具
运维·服务器·网络·windows·php·apache
澄岚明雪2 小时前
八股复习之计算机网络1
网络协议·tcp/ip·计算机网络·http·https·信息与通信
abcefg_h3 小时前
TCP与UDP的区别
网络·tcp/ip·udp
qqssss121dfd3 小时前
计算机网络(第8版,谢希仁)第一章习题解答
网络·计算机网络
q***38515 小时前
IP地址、子网掩码(NETMASK)和网关(Gateway)
tcp/ip·gateway·智能路由器