网络编程套接字

socket

socket是操作系统提供的socket api,其实不止有一套,而是有好几套

  1. 流式 套接字 -> 给TCP使用的
  2. 数据报 套接字  -> 给UDP使用的
  3. Unix域 套接字 -> 不能跨主机通信,只是本地主机上的·进程和进程之间的通信方式(现在使用的很少了)

TCP和UDP都是传输层协议,都是给应用层提供服务的,但是由于这两个协议,特点,差异非常大,因此我们就需要搞两套api来分别表示

特点:

  • TCP:有连接,可靠传输,面向字节流,全双工
  • UDP:无连接,不可靠传输,面向数据报,全双工

TCP UDP 对比

有连接 VS 无连接

  • 有连接就好比,打电话,得电话先打通了,然后才能说话:可以选择,接听,也可以选择直接挂掉,通信双方保存对方的信息
  • 无连接就好比,发短信/发微信,不需要“先接通”,直接上来就发,这个就没法拒绝了,通信双方不需要保存对方的信息 

连接(Connection)

计算机中还有一个术语(链接Link),计算机中谈到的“连接”是一个“抽象的概念”,生活中,谈到的“连接”,拿个什么东西,把两个东西给拴起来,铁索连环,计算机中的连接,认为是要连接的双方,各自保存对方的信息,此时就认为是建立了一个“抽象的连接”

可靠传输 VS 不可靠传输

首先,要区分,可靠(要传输的数据,尽可能的传输给对方,尽力而不是确保) != 安全(你的传输的数据,是否容易被黑客截获掉,一旦被截获之后,是否会造成严重的影响)

在当前网络通信的过程中没可能会存在很多意外情况,比如,丢包,A给B传输10个数据报,B实际上只收到9个,为啥会出现丢包?网络环境,太复杂了,A传输给B,中间可能会经理很多的交换机和路由器,进行转发,这些交换机和路由器,也不只是转发你的数据,要转发很多数据

比如某个交换机/路由器,非常繁忙,繁忙到要处理的数据量,已经超出了自身的硬件水平极限,此时,多出来的数据就无法转发,会被直接丢弃到

类似于交通网,某个路口,动不动就堵车,生活中,堵车只能一点一点堵着,计算机中,出现上述拥堵的情况,可能直接把数据给丢弃掉,而且丢包是随机的过程,无法预知,啥时候会出现丢包,也无法预知,哪个路由器/交换机会出现丢包,为了对抗丢包,就引入“可靠传输”,TCP就具备可靠传输特点,内部就提供了一系列的机制来实现可靠传输

即使如此,TCP也可能“尽可能”无法保证,数据报100%到达对端,极端情况下,会出现网线断开的情况,纵使你在软件上使出浑身解数,也无济于事

前几年有个新闻,支付宝挂了,当时杭州施工,修路,挖掘机一铲子把机房光纤给铲了

UDP则是不可靠传输,传输数据的时候,压根不关心,对方是否收到,发了就完了,可靠传输,是有代价的,最典型的,效率会大打折扣,UDP比TCP快

面向字节流 VS 面向数据报

文件操作,就是字节流的,字节流,比喻成水流一样,读写操作非常灵活,TCP和文件操作,具有相同的特点;面向数据报就不是了,传输数据的基本单位是一个个的UDP数据报,一次读写,只能读写一个完整的UDP数据报,不能搞半个,也不能搞一个半,对于写代码,影响非常明显

谈到,网络传输数据的基本单位,涉及到几个术语

  • 数据报 Datagram UDP
  • 数据段 Segment TCP
  • 数据包 Packet IP
  • 数据帧 Frame 数据链路层

其实这几个词是有差别的,但是这里不关心这里的差别,本质上都是0101构成的二进制数据,谈不上“类型”

全双工 VS 半双工

全双工:一条链路层,能够进行双向通信(TCP,UDP都是全双工),创建socket对象,既可以读,也可以写,但是计算机中也有些概念,是只能单向通信的

半双工:一条链路,只能进行单向通信,网线里有8根,网线是电信号,既可以从A到B,也可以从B到A

UDP

socket api都是系统提供的(不同系统,提供的api是不一样的,Java中对于系统的这些api进一步的封装了)

UDP的socket api重点是两个类:

DatagramSocket

系统中,本身就有socket这样的概念,DatagramSocket就是对于操作系统的socket概念的封装,系统中的socket,可以理解成是一种文件,文件是一个广义的概念,socket文件,就可以视为是“网卡”这种硬件设备的抽象表现形式,针对socket文件的读写操作就相当于针对网卡这个硬件设备进行读写,具有“遥控器属性”这样的概念,计算机中起了个专门的名字“句柄”

  • 此处,DatagramSocket就可以视为是“操作网卡”的遥控器,针对这个对象进行读写操作,就是在针对网卡进行读写操作
  • receive方法的参数也就是“输出型参数”
  • send方法以及close方法等等也有各自的功能,socket也是一种文件,用完了要关闭,不然会占据文件描述符表的一个表项

DatagramPacket

针对UDP数据报的一个抽象表示,一个DatagramPacket对象,就相当于一个UDP数据报,一次发送/一次接受,就是传输了一个DatagramPacket对象

网络程序,会有客户端,也有服务器,Echo称为“回显”,正常的服务器,你给他发不同的请求,会返回不同的响应,请求->响应,此处回显意思就是请求发了啥,响应就是啥,这个过程中,没有计算,也没有业务逻辑,最简单的客户端服务器程序,只是单纯的去认识socket api的用法

同时java.net是关于网络编程相关的api和类,同时抛出SocketException异常,本质上也是IO操作

public UdpEchoServer (int port) throws SocketException {
    private DatagramSocket socket = null;
    socket = new DatagramSocket(port);
}
//服务器程序,需要在程序启动的时候,把端口号确定下来

建立UDP服务器和客户端

客户端是主动的一方,服务器是被动的一方,客户端要能知道服务器在哪,才能主动的了

  • ip地址(服务器所在主机的ip)
  • port(一个主机上,有多个程序都要进行网络通信,就需要把哪个程序用哪个端口给明确下来),而且,还要确保,一个端口不能同时被两个或者多个进程来关联(不能重复)

实际运行程序的时候,端口号指定为多少呢?我们需要自行指定,但是你要确保

  1. 端口号是合法的:1-65535
  2. 你搞的端口号,不能和别的进程使用的端口号冲突

可以随便写一个端口号试试,如果程序运行,没有任何异常,说明是不冲突的

接下来,还需要让服务器,能够不停的处理请求,不停的返回响应,服务器不是“一锤子买卖”,会持续不断地来处理请求,返回响应(7*24)

DatagramPacket requestPacket = new DatagramPacket();
//进行receive操作,关键在于,这里的参数构造,需要搞一个空盘子,把空盘子,交给食堂打饭阿姨,阿姨给你//装饭
DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
//写代码的时候,很少会有这样的操作,持有数据的空间,需要额外的创建(但是socket编程原生api,就都是这种风格,这样的风格也延续到Java中了)

这个参数 (对象)是一个UDP数据报,就包含两个部分

  • 报头(通过类的属性来表示的)
  • 载荷(通过构造方法传递的字节数组,作为持有载荷的空间)

这个存储载荷的空间,人家DatagramPacket没有自己new,而是让你new好,交给他使用

报头中的东西不全是我们所设定的,我们所设定的仅为receive和send里面设定的

DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
// 为了方便在 java 代码中处理 (尤其是后面进行打印) 可以把上述数据报中的二进制数据, 拿出来, 构造成 String
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

String有一个版本的构造方法,通过字节数组来构造

  • 接受,构造一个DatagramPacket空的对象即可
  • 发送,则是有数据的对象,不光要有数据,还要有数据要发送给谁(谁给我发的请求,我就把响应发给谁)

由于这是UDP,UDP socket自身没有记录对方的ip和port等信息,但是没事,DatagramPacket这个对象里有

DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
  • requestPacket里面就包含了信息:我来自于哪个ip,哪个端口,拿过来之后,放到响应的packet对象中即可
requestPacket.getSocketAddress());
  • 这个方法返回的对象里面就包含了IP和Port
public abstract class SocketAddress implements java.io.Serializable {

    @java.io.Serial
    static final long serialVersionUID = 5215720748342549866L;

    /**
     * Constructor for subclasses to call.
     */
    public SocketAddress() {}
}
  • 由于SocketAddress是抽象类,上述方法实际上返回的是InetAddress对象,这个对象中就包含了IP和Port,还可以把这里的IP和端口真正打印出来
public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("请输入要发送的请求: ");
            // 1. 从控制台读取用户输入
            String request = scanner.next();
            // 2. 构造请求并发送
            // 构造请求数据报的时候, 不光要有数据, 还要有 "目标"
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, 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);
        }
}
  • 如果没有客户端发送请求,服务器的代码就会在receive这里阻塞,这里的阻塞都是系统内核控制的(系统原生的socket api,receive这里也是会阻塞的)
  • 一般的服务器程序都是这种死循环,如果想停止,直接强制结束,如果同时有多个客户端发来请求,都是内核这里先收到这些数据,咱们的代码目前就是一个线程,串行顺序的一个一个读出来
public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
    socket = new DatagramSocket();
    // 这俩信息需要额外记录下来, 以备后续使用.
    this.serverIp = serverIp;
    this.serverPort = serverPort;
}

客户端也需要端口号,在进行一次通信的过程中,需要知道至少4个核心指标

  • 源IP 发件人地址
  • 源端口 发件人电话
  • 目的IP 收件人地址
  • 目的端口 收件人电话

对于客户端来说,马上就要给服务器发请求,对于客户端给服务器发请求这个行为来说,刚才指定的服务器IP和服务器端口,是目的IP和目的端口

源ip,也是客户端所在的ip(也可以视为127.0.0.1),源端口是啥?构造socket对象的时候,没有指定端口号,没指定不代表没有,而是操作系统,自动分配了一个空闲的(不和别人冲突)的端口号过来了,这个自动分配的端口号,每次重新启动程序都可能不一样

  • 为啥服务器需要有固定的端口号,而客户端就需要让系统自动分配,反过来行不行?
  • 服务器要有固定端口号,是因为,客户端需要主动给服务器发请求,如果服务器端口号不是固定的(假设每次都变,此时客户端就不知道请求发给谁了)

  • 客户端为啥要系统自动分配,指定固定的端口号行不行呢?
  • 如果就给客户端指定固定的端口号是不行的,因为固定的端口号,是可能会和客户端所在电脑上其他程序冲突的,一旦端口冲突,就会导致你的程序启动不了了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值