0
点赞
收藏
分享

微信扫一扫

C语言实现冒泡排序

Python芸芸 03-17 16:31 阅读 2

网络编程:通过代码完成基于网络的跨主机通信

其中TCP/IP网络是日常编程中最常涉及到的,最通用的跨主机通信的方式


一些概念

客户端 VS 服务器

客户端:在网络中主动发起通信的一方

服务器:被动接受的一方


客户端和服务器之间的交互

客户端给服务器发送的数据,称为请求(request)

服务器返回给客户端的数据,称为响应(response)

1.一问一答

一个请求对应一个响应,进行web开发就是这种模式

2.一问多答

一个请求对应多个响应,涉及到下载的场景

3.多问一答

多个请求对应一个响应,涉及到上传的场景

4.多问多答

多个请求对应多个响应,涉及到远程控制的场景


TCP VS UDP

进行网络编程需要使用系统的API,本质上是由传输层提供的

涉及到TCP和UDP两个协议,两个协议差异很大

TCP特点:有连接;可靠传输;面向字节流;全双工

UDP特点:无连接;不可靠传输;面向数据报;全双工

连接

有连接:指抽象且虚拟的连接。连接的特点是双方都能认同,例如打电话就是有连接的通信方式

无连接:例如发微信/短信,无论你是否同意,我都能给你发过去

网络中的连接:通信双方有一些数据结构能各自保存对方的相关信息


传输可靠性

前提:无论使用什么技术,都无法100%保证网络数据能从A传到B

可靠传输:尽可能完成数据传输,无法确保对方是否收到,但发送方可以知道对方是否收到了

不可靠传输:就是不知道对方是否收到数据咯


面向字节流/数据报

面向字节流:和文件的字节流一致,网络中传输的数据基本单位是字节

面向数据报:传输(发送和接收数据)的基本单位是一个数据报(由一系列字节构成的特定的结构)


全双工

全双工:一个信道可以双向通信(类似日常见到的马路)

半双工:只能单向通信


UDP socket api的使用

核心的类有两个:DatagramSocket, DatagramPacket

操作系统中有一种文件叫socket文件,这种文件抽象表示了网卡这个硬件设备

通过网卡发送数据,就是写socket文件

通过网卡接收数据,就是读socket文件 

DatagramSocket

负责对socket文件进行读写

方法

DatagramPacket

表示一个UDP数据报

TCP相关的api

ServerSocket

对应到网卡的设备,这个类只能给服务器进行使用

Socket

既可以给服务器使用又可以给客户端使用


代码实操

回显服务器:UDP版本

服务器接收客户端的请求,返回响应;客户端发啥就响应啥

对于服务器来说

第一步:先创建DatagramSocket对象,接着操作网卡通过socket对象完成

socket对象存在内存中,针对这个内存对象进行操作就能影响到网卡

程序一启动就需要关联/绑定上一个操作系统的端口号(区分主机上进行网络通信的程序)

这个端口号需要我们手动指定

一个主机上的一个端口号只能被一个进程绑定,如果一个端口已经被进程1绑定了,进程2也想绑定就会失败(除非进程1把端口号释放出来了,比如进程1结束了)

这个异常表示socket创建失败,比如端口号被别人占用了

第二步:服务器的启动逻辑

第2.1步:创建while(true)

杀进程?服务器一般是在Linux系统上,而Linux结束一个进程使用kill命令

第2.2步:创建requestPacket保存数据报信息,接着读取请求更新

new byte[] 通过这个字节数组保存收到的消息正文,就是UDP数据报载荷部分 

receive从网卡中读取到一个UDP数据报,放到requestPacket对象中,其中数据报的载荷部分被放到requestPacket的内置字节数组中,报头部分可以被requestPacket其他属性保存

requestPacket还能知道数据从哪里来的(也就是源IP端口位置)

如果执行到receive的时候客户端还没发来请求,receive会暂时阻塞

上面代码的逻辑示意图

基于字节数组构造出String(无论是二进制数据还是文本数据,Java的String都可以保存)

从0号位置开始构造String,getLength获取到字节数组中有效数据的长度(不一定是4096)

第2.3步跳过

第2.4步:创建一个responsePacket作为响应对象,然后把响应返回给客户端

负责响应的packet对象里面不是空白的字节数组,而是直接把String里包含的字节数组给拎过来

构造对象第二个参数为何不能是response.length()? -->这个单位是字符,而原代码的单位是字节

第三个参数:requestPacket:客户端来的数据报

getSocketAddress()方法可以得到INetAddress对象,这个对象就包含了IP和端口和服务器通信对端(对应的客户端)的IP和端口

原理:把请求中的源IP,源端口作为响应的目的IP和目的端口,可以达到把消息返回给客户端的效果了

从上面的代码也可以看出:UDP是无连接的通信,体现在UDP的socket本身不保存对端的IP和端口,而是保存在每个数据报;另外,代码中没有体现建立连接和接收连接这样的操作

全双工怎么体现?一个socket对象既可以发送也可以接收

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

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(true){
            //1.读取请求更新
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //读到的字节数组转换成String,方便后序逻辑处理
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //2.根据请求计算响应(回显服务器不用做这一步)
            String response = process(request);
            //3.把响应返回给客户端
            //构造一个DatagramPacket作为响应对象
            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);
        }
    }
    //此处是回显服务器,可以直接return
    publice String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);//通常使用的端口,大于1024,小于65535
        //注意端口别被其他进程占用
        server.start();
    }
}

对于客户端来说

第一步:创建socket对象

此处不需要手动指定端口号,因为系统会自动分配一个空闲的端口号,因为无法确保手动指定的端口号是否被别人占用

客户端要给服务器发起请求,前提是知道服务器在哪,要把服务器ip和端口找到

请求的源ip就是本机,源端口就是系统分配到

第二步:从控制台读取要发送的请求数据

最好用next()而不是nextLine(),因为使用nextLine()读取要手动输入换行符,也就是按enter键,由于enter键不仅仅会产生\n,还会产生其他字符,就会使读取到的内容有问题

next()是以空白符作为分割符,包括不限于换行,回车,空格,制表符等

第三步:构造请求并发送

第三个参数什么意思?因为我们初始化的ip采用的是字符串(点分十进制格式),而这里的操作都是用字节,所以要把serverIp转成字节形式

第四步:读取服务器响应

第五步:把响应显示到控制台上

package network;

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

public class UdpEchoClient {
    DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;
    //此处ip使用的是字符串,类似点分十进制
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while(true){
            System.out.println("->");//提示用户接下来输入的内容
            //1.从控制台读取要发送的请求数据
            if(!scanner.hasNext()){
                break;
            }
            String request = scanner.next();
            //2.构造请求并发送
            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);
            //4.把响应显示到控制台上
            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", 9090);
        client.start();
    }
}

总结:三种DatagramPacket对象创建方法

 启动程序


整个流程

1.服务器启动。启动之后,立即进入 while 循环,执行到 receive,进入阻塞,此时没有任何客户端发来请

2.客户端启动。启动之后,立即进入 while 循环,执行到 hasNext 这里,进入阻塞,此时用户没有在控制台输入任何内容

3.用户在客户端控制台中输入字符串,按下回车。此时 hasNext 阻塞解除,next 会返回刚才输入的内容.基于用户输入的内容,构造出一个 DatagramPacket 对象,并进行 send。send 执行完毕之后,继续执行到 receive 操作,阻塞等待服务器返回的响应数据(此时服务器还没返回响应呢,这里也会阻塞),服务器收到请求之后,就会从 receive 的阻塞中返回.

4.返回之后,就会根据读到的 DatagramPacket 对象,构造 String request,通过 process 方法构造一个 String response,再根据 response 构造一个 DatagramPacket 表示响应对象,再通过 send 来进行发送给客户端。执行这个过程中,客户端也始终在阻寒等待。

5.客户端receive 中返回执行,就能够得到服务器返回的响应,并且打印倒控制台上于此同时, 服务器进入下一次循环,也要进入到第二次的 receive 阻塞, 等待下个请求了


回显服务器改进:词典服务器

package network;

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

public class UdpEchoClient {
    DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;
    //此处ip使用的是字符串,类似点分十进制
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while(true){
            System.out.print("->");//提示用户接下来输入的内容
            //1.从控制台读取要发送的请求数据
            if(!scanner.hasNext()){
                break;
            }
            String request = scanner.next();
            //2.构造请求并发送
            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);
            //4.把响应显示到控制台上
            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", 9090);
        client.start();
    }
}


TCP版本

服务器代码

第一步:创建serverSocket

第二步:由于TCP是有连接的,和打电话一样,需要客户端拨号,服务器来接听

调用accept()方法建立连接

有一个客户端连进来了,accept只能返回一次结果;如果有若干个客户端要连进来,那我们就需要多次使用accept,所以我们用while(true)包裹起来

第三步:我们创建一个方法处理连接

3.1步:告诉用户客户端的IP和端口

3.2步:对socket进行读取和写入操作

TCP是面向字节流的,和文件中的字节流一样。所以可以用文件操作中的类来针对TCP的socket进行读写

inputStream是从网卡上读数据,outputStream是往网卡中写数据

现在读操作有两条路可以走:

1.使用read方法进行读取,读取到的数据存储到byte数组中,后续根据请求处理响应再把这个byte数组转成String

2.用Scanner方法读取inputStream的内容,直接就是字符串的形式,方便处理

这里我们用第二种方法

3.2.1步:读取请求并解析

3.2.2步:根据请求计算响应(process方法和UDP里的一模一样)

3.2.3步:把响应返回给客户端

第一种方法:用write()

但是这种方法不方便给返回的响应中添加\n

第二种方法:用PrintWriter类,相当于把字节流转成字符流

println方法可以自动在结尾加上一个换行

package network;

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;

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){
            //通过accept方法来接听电话
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }
    //通过这个方法处理一次连接,建立连接的过程会涉及多次请求响应交互
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线", clientSocket.getInetAddress(), clientSocket.getPort());
        //循环读取客户端请求,保存对端信息
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
            Scanner scanner = new Scanner(inputStream);
            while(true){
                //通过scanner读取数据
                if (!scanner.hasNext()){
                    //读取完毕,客户端断开连接,就打印读取完毕
                    System.out.printf("[%s:%d] 客户端下线", clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //1.读取请求并解析。next读到空白符才会结束
                //因此要求客户端发来的请求必须带有空白符结尾
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应返回给客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                System.out.printf("[%s:%d] req: %s, resp:%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

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

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

客户端代码

执行画横线这行代码就会和对应的服务器进行tcp的连接建立流程(这是在系统内核中完成)

内核中连接的流程走完,服务器就能从accept返回

package network;

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 {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        //由于tcp是有链接的,此处可以把这里的ip和port直接传给socket对象
        socket = new Socket(serverIp,serverPort);
    }
    public void start(){
        System.out.println("客户端启动");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            Scanner scannerConsole = new Scanner(System.in);//控制台
            Scanner scannerNetWork = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while (true){
                //1.从控制台读取输入的字符串
                if(!scannerConsole.hasNext()){
                    break;
                }
                System.out.println("->");
                String request = scannerConsole.next();
                //2.把请求发给服务器,使用println来发送,防止发送末尾带有\n
                writer.println(request);
                //3.从服务器中读取响应
                String response = scannerNetWork.next();
                //4.把响应显示出来
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

客户端退出之后,服务器就能感知到客户端下线的操作

原理:

客户端退出的时候,这里的inputStream就会中断,scanner.hasNext()感知不到下一个数据直接返回false,整个循环就退出了


问题1

上述代码中,在客户端中输入hello,服务器没有响应

出现上面的情况,本质原因是PrintWriter内置的缓冲区在搞怪

为什么会有缓冲区呢?

我们知道IO操作是比较低效的操作,引入缓冲区,可以把多次写入网卡的数据攒起来一波统一发送,也就是把多次IO合并成1次,这样可以提高效率

缓冲区有个特性,如果发送的数据很少,还没填满缓冲区,数据就会待在缓冲区里无法被发送出去

简单的解决方法:手动冲刷缓冲区,调用flush方法

 


问题2

需要close吗?

其实是需要的,需要针对clientSocket进行close操作。因为TCP中的client socket每个客户端都有一个,随着客户端越来越多,这里消耗的socket也越来越多,如果不加释放就可能把文件描述符表占满

对于serverSocket来说,整个程序只有唯一一个对象,并且这个对象的生命周期跟随整个程序,无法提前关闭。随着进程的销毁会一起被释放,所以不需要手动close

对于UDP来说,DatagramSocket也是只有一个对象,也可以不用close


问题3 


整个过程


举报

相关推荐

0 条评论