Java网络编程(二)Socket用法浅学

自己总结的,难免有错,欢迎指出,在下感谢不尽!

第二章 Socket用法浅学

前言

Java网络程序致力于实现应用层,传输层向应用层提供了Socket(套接字)类,Socket封装了下层的数据传输细节,应用层的程序通过Socket来建立和远程主机的连接,以及进行数据的传输。

Socket类的种类和应用:

使用的协议 Socket类 源主机 远程主机 谁先启动
UDP协议 DatagramSocket 发送端 接收端 java.net 都行
TCP协议 Socket、ServerSocket 客户端 服务器端 java.net 服务器

基于以上,Java网络编程又叫Socket编程

2.1 IP地址的封装对象InetAddress

2.1.1、InetAddress

类名 父类 子类 使用的协议 实现接口(都为序列化)
InetAddress Object Inet4Address、Inet6Address 这个类表示一个互联网协议(IP)地址。 Serializable
public final class Inet4Address InetAddress 这个类表示一个互联网协议版本4 (IPv4)地址 Serializable
public final class Inet6Address InetAddress 这个类表示一个互联网协议版本6 (IPv6)地址 Serializable

InetAddress类表示服务器的IP地址,InetAddress提供了一系列的静态工厂方法用于返回实例

一个InetAddress实例由一个IP地址和可能的对应的主机名构成(取决于它是否是一个主机名或是否已经做了反向主机名解析构造)。

静态工厂方法

1、常用静态工厂方法

  • 1.1、static InetAddress getByName(String host)
    如 String host=“www.baidu.com”,或者host=“192.168.1.1” 确定主机的IP地址,给定主机名。
  • 1.2、static InetAddress[] getAllByName(String host)
    如新浪的是集群服务器 ,“www.sina.com"或者"111.111.111.1"什么的
    给定一个主机的名称,返回其IP地址的数组,基于系统上的配置的名称服务。
    有一些主机集群了,使得其ip有很多。其中的addr为一个字节数组 ,
    参数按网络字节顺序:地址的高位字节位于 getAddress()[0] 中,
    IPv4 地址 byte 数组的长度必须为 4 个字节,IPv6 byte 数组的长度必须为 16 个字节。
  • 1.3、static InetAddress getByAddress(String host, byte[] addr)
    它创造了一个基于所提供的主机名和IP地址。
  • 1.4、static InetAddress getByAddress(byte[] addr)
    返回给定的原始IP地址 InetAddress对象。
  • 1.5、static InetAddress getLocalHost()
    返回本地主机的地址。 是IP地址对象

常用方法:

2、常用的获取方法

  • 2.1、String getHostAddress()
    返回文本表示中的IP地址字符串。 主机IP地址的字符串形式
  • 2.2、String getHostName()
    获取此IP地址的主机名。 主机名字符串
  • 2.3、public String getCanonicalHostName()
    获取此IP地址的完全限定的域名。最好的工作方法,这意味着我们可能无法返回FQDN取决于底层的系统配置。
  • 2.4、boolean isReachable(int timeout) 方法
    测试是否可以达到该地址
  • 2.5、toString() 方法
    将此 IP 地址转换为 String

Demo

public class Practice1_InetAddress {
    public static void main(String[] args) throws IOException {
        getInstance1(getInstance3());
        getInstance2();
    }

    /**
     * 使用静态工厂方法1构造一个IP对象,
     * static InetAddress getByName(String host)
     * @return byte[] 返回ip地址的byte数组。
     */
    private static byte[] getInstance3() throws IOException {
        System.out.println("getInstance1_getByName");
        InetAddress ip = InetAddress.getByName("www.qq.com");//可以是域名形式www.qq.com 主机IP地址:112.53.27.11

        //注意,这里使用的什么形式构造ip对象,则getHostName就返回什么。

        //获取其IP地址和对应的主机名
        String str_ip = ip.getHostAddress();//对应Ip网址的主机IP地址,主机IP地址:112.53.27.11
        String str_host = ip.getHostName();//对应的主机名www.qq.com
        System.out.println("主机名:" + str_host + " 主机IP地址:" + str_ip);
        System.out.println(ip.getCanonicalHostName());//获取此 IP地址的完全限定域名 112.53.27.11


        boolean reachable=ip.isReachable(2000);//获取布尔类型,看是否能到达此IP地址
        System.out.println("是否能达到该IP地址:"+reachable);
        byte[] b;
        b = ip.getAddress();
        for(byte f:b){
            System.out.print(f+" ");//[B@735b478 [B@735b478 [B@735b478 [B@735b478
        }
        System.out.println();
        return b;

    }

    /**
     * 演示静态方法static InetAddress getLocalHost()
     * 返回本地主机的地址。  是IP地址对象
     */
    private static void getInstance2() throws UnknownHostException {
        System.out.println("getInstance2_getLocalHost");
        InetAddress host = InetAddress.getLocalHost();
        String ip = host.getHostAddress();
        String name = host.getHostName();
        System.out.println("name:"+name+" ip:"+ip);//name:滚被单fhg ip:192.158.13.1
    }

    /**
     * 演示静态工厂方法static InetAddress getByAddress(byte[] addr)
     *返回给定的原始IP地址 InetAddress对象。
     * @param bytes 传入的IP地址的byte数组。
     * @throws IOException 可能抛出的异常
     */
    private static void getInstance1(byte[]bytes) throws IOException {
        System.out.println("getInstance3_getByAddress");
        InetAddress ip = InetAddress.getByAddress(bytes);
        //注意,这里使用的什么形式构造ip对象,则getHostName就返回什么。

        //获取其IP地址和对应的主机名
        String str_ip = ip.getHostAddress();//对应Ip网址的主机IP地址,主机IP地址:112.53.27.11
        String str_host = ip.getHostName();//对应的主机名www.qq.com
        System.out.println("主机名:" + str_host + " 主机IP地址:" + str_ip);
        System.out.println(ip.getCanonicalHostName());//获取此 IP地址的完全限定域名 112.53.27.11


        boolean reachable=ip.isReachable(2000);//获取布尔类型,看是否能到达此IP地址
        System.out.println("是否能达到该IP地址:"+reachable);

    }
}

2.1.2、抽象类SocketAddress

这个类表示没有协议附件的套接字地址。作为一个抽象类,它意味着要用特定的、依赖协议的实现子类化。它提供了套接字用于绑定、连接或作为返回值的不可变对象。

父类 子类 协议 实现接口
public abstract class SocketAddress Object InetSocketAddress 这个类表示没有协议附件的套接字地址。 Serializable
InetSocketAddress SocketAddress IP协议 Serializable

InetSocketAddress类

这个类实现了一个IP套接字地址(IP地址+端口号),它也可以是一对(主机名+端口号),在这种情况下,将尝试解析主机名。如果解析失败,那么该地址被认为是无法解析的,但是在某些情况下仍然可以使用,比如通过代理进行连接。
它提供了套接字用于绑定、连接或作为返回值的不可变对象。
通配符是一个特殊的本地IP地址。它通常表示“any”,只能用于绑定操作。

构造器:

类别 描述
InetSocketAddress​(int port) 创建一个套接字地址,其中IP地址为通配符地址,端口号为指定值。
InetSocketAddress​(String hostname, int port) 从主机名和端口号创建套接字地址。
InetSocketAddress​(InetAddress addr, int port) 根据IP地址和端口号创建套接字地址。

常用方法:

  • 1.1、static InetSocketAddress createUnresolved​(String host, int port)
    从主机名和端口号创建一个未解析的套接字地址。
  • 1.2、InetAddress getAddress()
    获取地址
    1.3、String getHostName()
    获取主机名。
    1.4、String getHostString()
    返回主机名,如果没有主机名,则返回地址的字符串形式(它是使用文字创建的)。
  • 1.5、int getPort()
    获取端口号
    1.6 int hashCode()
    Returns a hashcode for this socket address.
  • 1.7、boolean isUnresolved()
    检查地址是否已被解析。
  • 1.8、String toString()
    构造这个InetSocketAddress的字符串表示形式。

2.2 UDP协议及DatagramSocket类

2.2.1、UDP协议

在TCP/IP参考模型中,传输层的协议可使用TCP协议或者UDP协议

UDP协议
UDP是一个无连接协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。
UDP 是一个简单的传输层协议。和 TCP 相比,UDP 有下面几个显著特性:

  • UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
  • UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界。
  • UDP 是无连接的。UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程。
  • UDP 支持多播和广播。
  • UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包而言UDP的额外开销很小。

应用场景
在选择UDP作为传输协议时必须要谨慎。在网络质量令人十分不满意的环境下,UDP协议数据包丢失会比较严重。但是由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。比如我们聊天用的ICQ和QQ就是使用的UDP协议。如果使用TCP传输实时音频,由于速度慢 ,会导致接收方时常出现停顿,声音断断续续,这是无法容忍的。由此,UDP更加比TCP适于传输实时音频。
在这里插入图片描述

2.2.2、DatagramSocket类用法

public class DatagramSocket extends Object implements Closeable

描述
这类代表一个发送和接收数据包的插座。 数据报套接字发送或者接收点的分组传送服务。每个发送的数据包或数据报套接字上接收单独寻址和路由。从一台机器发送到另一台机器的多个数据包可能会被不同的路由,并可以以任何顺序到达。
在可能的情况下,一个新建的DatagramSocket有SO_BROADCAST套接字选项已启用,以便允许广播数据报传输。为了收到广播包应该将DatagramSocket绑定到通配符地址。在一些实施方案中,广播包,也可以接受当一个DatagramSocket绑定到一个更具体的地址。
构造器

类型 描述
DatagramSocket() 构建一个数据报套接字绑定到本地主机的任何可用的端口。
protected DatagramSocket(DatagramSocketImpl impl) 创建一个绑定的数据报套接字与指定的datagramsocketimpl。
DatagramSocket(int port)《常用》 构建一个数据报套接字绑定到本地主机的指定端口。
DatagramSocket(int port, InetAddress laddr) 《常用》 创建一个数据报套接字,绑定到指定的本地地址。
DatagramSocket(SocketAddress bindaddr) 创建一个数据报套接字,绑定到指定的本地套接字地址。

常用方法

1、 判断

  • 1.1 boolean isBound()
    返回套接字的绑定状态。
  • 1.2 boolean isClosed()
    返回套接字是否关闭或不关闭的。
  • 1.3 boolean isConnected()
    返回套接字的连接状态。

2、发送接受数据报

  • 2.1 void receive(DatagramPacket p)
    接收数据报包从这个插座。
  • 2.2 void send(DatagramPacket p)
    从这个套接字发送数据报包。

3、 获取端口和IP地址

  • 3.1 InetAddress getInetAddress()
    返回此套接字连接的地址。
  • 3.2 InetAddress getLocalAddress()
    获取绑定的套接字的本地地址。
  • 3.3 int getLocalPort()
    返回此套接字绑定的本地主机上的端口号。
  • 3.4 int getPort()
    返回此套接字连接的端口号。
  • 3.5 SocketAddress getLocalSocketAddress()
    返回此套接字绑定到的端点的地址。

4 连接和关闭

  • 4.1 void close()
    关闭该数据报套接字。
  • 4.2 void connect(InetAddress address, int port)
    将套接字连接到这个套接字的远程地址。
  • 4.3 void connect(SocketAddress addr)
    将此套接字连接到远程套接字地址(IP地址+端口号)。
  • 4.4 void disconnect()
    断开插座。

2.2.3、数据报DatagramPacket类

描述
UDP的数据都是以数据报包来发送的,大小限制在64K以内。

  • IPv4的数据报包理论上最大长度65507字节(约64K)
  • IPv6的数据报包理论上最大长度65536字节(64K)
  • 许多基于UDP的协议都规定数据报的长度,如DNS:不超过512字节;TFTP:不超过512字节;NFS:最大为8KB;多于数据报的最大长度,网络会将数据截断、分片、或丢弃部分数据。而这种情况java程序是收不到任何通知的!
    在这里插入图片描述
    数据报包封装为DatagramPacket类:
 public final class DatagramPacket extends Object
  • 这类表示一个数据报包。一个完整的UDP数据报包包括IP头、UDP头和数据3部分。UDP头部分包括了源端口、目标端口、UDP长度和检验和,数据包的长度就是指数据这部分的长度。
  • 数据包是用来实现一个无连接的分组传送服务。每个消息都是从一台机器路由到另一个完全基于包含在该数据包内的信息。
  • 从一台机器发送到另一台机器的多个数据包可能会被不同的路由,并可能以任何顺序到达。包交付没有保证。

构造器
构造用于接收数据的对象

  • public DatagramPacket(byte[]data,int length);
    data是用来存放数据的数组,length指定接受的字节长度。
  • public DatagramPacket(byte[]data,int offset,int length);
    同上,offset指定数据从哪个角标开始存放,data[offset]。

构造用于发送数据的对象

  • public DatagramPacket(byte[]data,int offset,int length,InetAddress address,int port);
    要指明要发送的主机IP地址和端口
  • public DatagramPacket(byte[]data,int offset,int length,SocketAddress address);
    要指明要发送的address,SocketAddress类型

常用方法:

InetAddress getAddress()
返回的IP地址的机器,这个数据包被发送或从收到的数据报。

  • byte[] getData()
    返回数据缓冲区。
  • int getLength()
    返回要发送的数据的长度或收到的数据的长度。
  • int getOffset()
    返回所要发送的数据的偏移量或所接收的数据的偏移量。
  • int getPort()
    返回远端端口号,这个数据包被发送或从收到的数据报。
  • SocketAddress getSocketAddress()
    获取SocketAddress(通常是IP地址+端口号)的远程主机,数据包被发送到或来自。
  • void setAddress(InetAddress iaddr)
    设置本机的IP地址,这个数据包被发送。
  • void setData(byte[] buf)
    为这个数据包设置数据缓冲区。
  • void setData(byte[] buf, int offset, int length)
    为这个数据包设置数据缓冲区。
  • void setLength(int length)
    为这个数据包设置长度。
  • void setPort(int iport)
    设置远端端口号,这个数据包被发送。
  • void setSocketAddress(SocketAddress address)
    集SocketAddress(通常是IP地址+端口号)的远程主机的数据报发送。

2.2.4、使用DatagramSocket实现简单信息交换

需求:使用UDP,发送端发送信息(键盘录入)给接收端,接收端将信息打出来。

public class Practice2_DatagramSocket_UDPSend {
    private DatagramSocket UdpClient;//服务端套接字

    /**
     * 有参构造器,UDP发送端初始化
     * @param port 发送端绑定的本地端口
     */
    public Practice2_DatagramSocket_UDPSend(int port) throws IOException {
        System.out.println("UDP发送端初始化,端口为"+port);
        this.UdpClient = new DatagramSocket();//发送端不用指定端口
        System.out.println("端口绑定完毕!发送端启动....");
        System.out.println("请输入你要发送的信息:");
        //sendDate(port);
    }
    public void sendDate(int port) throws IOException {
        //读取键盘录入信息,发送到接收端
        //1.需要键盘录入信息并存储到一个缓冲区
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String line;//=null
        //2.将信息封装为数据包——DatagramPacket
        //发送数据封装为数据包
        DatagramPacket dataPacket;//=null;
        while((line=reader.readLine())!=null){
            byte[]bytes = line.getBytes();
            dataPacket = new DatagramPacket(bytes,bytes.length,
                    InetAddress.getByName("192.168.137.1"),port);//从主机的port端口发送数据包
            //x.x.x.255是广播ip,此时x.x.x.2发信息到广播IP,则x.x.x.0~x.x.x.255的IP都能收到,这就是广播。
            //3.发送数据包
            UdpClient.send(dataPacket);
            System.out.println("send successfully:"+line);
            if(line.equals("bye")){
                break;
            }
        }
        //5.关闭DatagramSocket流
        UdpClient.close();
        System.out.println("发送端退出!");
    }
    public static void main(String[]args) throws IOException {
        int port = 11010;
        new Practice2_DatagramSocket_UDPSend(port).sendDate(port);
    }
    
}

发送端

 /**
 * 接收端,接受UDP发送过过来的信息,并打印出来
 *
 */

public class Practice2_DatagramSocket_UDPReceive {
    private DatagramSocket UdpServer;
    Practice2_DatagramSocket_UDPReceive(int port) throws SocketException {
        System.out.println("接收端启动!接收端的端口号为:"+port);
        UdpServer = new DatagramSocket(port);

        //receiveData();

    }

    public void receiveData() {
        while(true){
            try{
                DatagramPacket data;
                byte[]bytes = new byte[1024];
                if(UdpServer!=null){
                    data = new DatagramPacket(bytes,bytes.length);
                    UdpServer.receive(data);//这个是阻塞式方法,与read()类似。
                    int port = data.getPort();
                    String hostName = data.getAddress().getHostName();//获取发送端的主机名
                    System.out.println("收到发送端信息!");

                    System.out.println("端口"+port+" 主机名:"+hostName+"\n消息为:"
                            + new String(data.getData(),0,data.getLength()));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[]args) throws IOException {
        new Practice2_DatagramSocket_UDPReceive(11010).receiveData();//10010被占用则报错!

    }
}

运行结果
发送端
在这里插入图片描述
接收端:
在这里插入图片描述

2.3 TCP协议及Socket、ServerSocket

2.3.1、TCP协议

  • TCP 提供一种面向连接的、可靠的字节流服务
  • 在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP
  • TCP 使用校验和,确认和重传机制来保证可靠传输
  • TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
  • TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制

注意:TCP 并不能保证数据一定会被对方接收到,因为这是不可能的。TCP 能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此准确说 TCP 也不是 100% 可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。

由于暂时还不能深入了解TCP协议,这里就插一个链接TCP (传输控制协议)

2.3.2、Socket类创建客户端

描述

 public class Socket
  extends Object implements Closeable

这个类实现了客户端套接字(也被称为“套接字”)。套接字是两台机器之间的通信的一个端点。
套接字的实际工作是由该类的一个实例进行SocketImpl。一个应用程序,通过改变创建套接字实现的套接字工厂,可以配置自己创建适合本地防火墙的套接字。
1、Socket类的构造器

  • 1、构造器:
  • 1.1、Socket(String host, int port)
  • 创建一个流套接字,并将其与指定的主机上的指定端口号连接起来。
  • 1.2、Socket()
  • 创建一个连接的套接字,与socketimpl系统默认的类型。
  • 1.3、 Socket(InetAddress address, int port)
  • 创建一个流套接字,并将其与指定的IP地址中的指定端口号连接起来。
  • 1.4、Socket(Proxy proxy)
  • 创建一个连接的套接字类型,指定代理,如果有,应该使用无论任何其他设置。
  • 1.5、protected Socket(SocketImpl impl)
  • 创建一个用户指定的socketimpl连接插座。
  • 1.6、Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
    创建一个指定IP和端口的套接字,并将其与指定的远程端口上的指定的远程地址连接起来。
  • 1.7、Socket(String host, int port, InetAddress localAddr, int localPort)
    创建一个指定IP和端口套接字,并将其连接到指定的远程端口上的指定的远程主机上。

2、如何发送数据?
Socket类和ServerSocket类它们的数据是可以说是在一个IO流里边传输。

  • 2.1、InputStream getInputStream()
    返回此套接字的输入流。
  • 2.2、OutputStream getOutputStream()
    返回此套接字的输出流

此时我们就将发送接受数据回归到了操作IO流里边了。
3、Socket如何获取IP、端口等信息

获取本地IP、远程IP、本地输入流、输出流、关闭Socket流

  • 3.1、InetAddress getInetAddress()
    返回套接字连接的地址。
  • 3.2、InetAddress getLocalAddress()
    获取绑定的套接字的本地地址。
  • 3.3、void close()
    关闭这个套接字。

获取本地端口、连接的远程端口

  • 3.4、int getPort()
    返回此套接字连接的远程端口号。
  • 3.5、int getLocalPort()
    返回此套接字绑定的本地端口号。
  • 3.6、void connect(SocketAddress endpoint)
    将此套接字连接到服务器。
  • 3.7 void connect(SocketAddress endpoint, int timeout)
    将此套接字与指定的超时值连接到服务器。
  • 3.8 void bind(SocketAddress bindpoint)
    将套接字绑定到本地地址。

4、、客户端的IP地址如何设置
在一个Socket对象中,既包含远程服务器的IP地址和端口信息,又包含本地客户的IP地址和端口信息。默认情况客户端的ip地址来自客户端的主机,端口随机分配。

我们显示设置客户端的IP地址和端口可使用Socket构造方法1.6、1.7。

如果一个主机同属于两个以上的网络,它就可能拥有多个IP地址。如,一个主机在Internet网络的IP地址是222.43.1.34,在一个局域网的IP地址是“112.5.4.5”,假定该主机上的客户程序希望和同一个局域网的一个服务器程序(112.5.4.45:8000)通信,则可如下构建客户端Socket对象:

InetAddress remoteAddr = InetAddress.getByName("112.5.4.45");
InetAddress localAddr = InetAddress.getByName("112.5.4.5");
Socket socket = new Socket(remoteAddr,8000,localAddr,2345);//2345表示客户端的端口。

5、设置等待建立连接的超时时间
因为TCP协议的原因,客户端的Socket构造方法请求和服务器连接的时候,可能要等待一段时间,默认情况下,阻塞状态,一直等,直到连接成功或者抛出异常。Socket连接时毫无疑问受网速的影响,可能会长时间处于等待状态,这个时候我们可设置客户端尝试连接的时间。

Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);//服务器是本地主机,端口8000
socket.connect(remoteAddr,60000);//等到连接的超时时间设置为1分钟,60000ms

一分钟内连接到了服务器,则connect成功返回;若1分钟内出现了某种异常则抛出该异常,若1分钟后都没连接上去,则抛出SocketTimeoutException。时间如果设置为0,则永远不会超时。

6、实例之客户端创建
需求:向服务器发送请求,服务器响应并发送反馈信息。

public class Practice1_Socket_Client {
    private Socket client;

    /**
     * 1、Socket(String host, int port)
     * 创建一个流套接字,并将其与指定的主机上的指定端口号连接起来。
     * 输入bye退出连接,输入send发送请求。
     *
     * @param port 传入的端口号——服务器端口
     * @param host 指定主机——服务器主机
     */
    public Practice1_Socket_Client(String host, int port) throws IOException {
        System.out.println("客户端启动...");
        client = new Socket(host, port);
    }

    public void talk() throws IOException {//向服务端发送请求。得到的响应封装为流返回。
        OutputStream out = client.getOutputStream();
        BufferedWriter bufr = new BufferedWriter(new OutputStreamWriter(out));
        String mess = "我来了";
        //out.write(mess.getBytes());//非阻塞方法,因为直接写字节。如改为BufferedWriter,不加刷新发送不出去。
        bufr.write(mess);
        //################
        // 不加刷新,这个方法只是把mess写到了out里边,没有发送出去。进而阻塞了服务器那边的读取,
        //服务器一直在读 in.read(bytes);这个方法会阻塞。
        bufr.flush();

        //接受服务器的应答,实现客户和服务器的交互

        InputStream in = client.getInputStream();

        byte[]bytes = new byte[1024];
        int len = in.read(bytes);//同理,如果服务器没有发送数据,这个方法也会阻塞。
        System.out.println(new String(bytes,0,len));

        client.close();

    }

    public static void main(String[] args) throws IOException {
        Practice1_Socket_Client client = new Practice1_Socket_Client("localhost", 9000);
        client.talk();
    }
}

2.3.3、ServerSocket类创建服务器端

描述

public class ServerSocket
extends Object implements Closeable

这个类实现了服务器套接字。服务器套接字等待来自网络的请求。它基于该请求执行某些操作,然后可能向请求者返回结果。 服务器套接字的实际工作由SocketImpl类的一个实例进行。一个应用程序可以更改创建套接字实现的套接字工厂,以配置自己创建适合本地防火墙的套接字。

1、ServerSocket的构造器

  • ServerSocket()
    创建一个绑定服务器套接字。
  • ServerSocket(int port)
    创建一个服务器套接字,绑定到指定的端口。
  • ServerSocket(int port, int backlog)
    创建一个服务器套接字,并将其绑定到指定的本地端口号,并使用指定的积压。
  • ServerSocket(int port, int backlog, InetAddress bindAddr)
    用指定的端口创建一个服务器,听积压,和本地IP地址绑定到。

port是服务器要绑定的端口号,backlog是客户连接请求队列的长度、bindAddr指定服务器所绑定的ip地址。

2、如何监听客户端的请求
方法:
ServerSocket.accept();从连接请求的队列里边取出一个客户的请求连接,然后创建与该客户连接的Socket对象,并将它返回。如果没有客户连接请求,则该方法会一直等待(阻塞式方法)。直到收到连接请求才能返回。
3、收发数据
同客户端。使用Socket的方法:

  • Socket.getInputStream()
  • Socket.getOutputStream()
    这样,通过流建立了传输通道,就能与客户进行数据的交互!注意!当服务器正在进行发送数据的操作,如果客户端断开连接,服务器会抛出一个IOException的子类SocketException异常:
java.net.SocketException:Connection reset by peer

这是服务器与单个客户通信发送的异常,应该要捕抓,使得服务器继续和其他的客户端通信。

3、实例之服务器端的创建
需求:向服务器发送请求,服务器响应并发送反馈信息。

/**
 * TCP之服务器端
 * 使用ServerSocket类
 */

public class Practice1_ServerSocket_Server {
    private ServerSocket serverSocket;

    public Practice1_ServerSocket_Server(int port) throws IOException {
        System.out.println("服务器启动!");
        serverSocket = new ServerSocket(port);
    }
    public void receiveServer(){
        while (true) {//服务器一般是不断进行监听的,有客户请求,就做出响应。
            Socket socket = null;
            try{
                socket = serverSocket.accept();//等待客户端连接。一旦返回socket对象意味着与一个客户连接。
                System.out.println("New connection accepted" + socket.getInetAddress() +
                        ":" + socket.getPort());
                InputStream in = socket.getInputStream();
                byte[]bytes = new byte[1024];
                in.read(bytes);
                System.out.println(new String(bytes));
                //服务器做出应答:
                OutputStream out = socket.getOutputStream();
                out.write("收到".getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
   }
    public static void main(String[]args) throws IOException {
        Practice1_ServerSocket_Server server = new Practice1_ServerSocket_Server(9000);//必须服务器先启动。
        server.receiveServer();
    }
}

不同于UDP,TCP传输中必须先启动服务器,而且服务器一般昼夜运行。

运行结果:
1、启动服务器:
在这里插入图片描述
2、启动客户端:
立马收到了服务器的响应:“收到"
在这里插入图片描述
服务器收到客户端的“我来了”请求:
在这里插入图片描述

2.3.4、实战上传文件(图片、视频)服务

需求:创建一个服务器,满足用户上传文件的需求。上传的文件可以是图片,文本等。服务器将数据保存到本地。同时服务器告知客户端上传成功。
1、创建客户端

/**
 * 客户端上传文件,然后服务器进行上传成功的响应!
 * 文件类型不限于文本、图片、音乐。
 *
 */

public class Practice3_TCP_FileUpload {
    public static void main(String[]args) throws IOException {
        System.out.println("客户端启动!");
        //1、创建客户端,要指定服务器的端口,主机地址
        Socket client = new Socket("localhost",10111);//主机为本机。端口10111

        //2、获取要上传的文件,开始上传
        OutputStream out = client.getOutputStream();

        //用于读取文件的流
        FileInputStream fileinput = new FileInputStream(new File("E:\\socket1.JPG"));

        //3、开始发送数据
        byte[]bytes = new byte[1024*1024];//分配1MB大小的内存
        int len=0;
        while((len=fileinput.read(bytes))!=-1){
            out.write(bytes,0,len);//使劲的发送到客户端。字节流不需要刷新?
        }

        //写完以后要告诉服务器
        client.shutdownOutput();

        InputStream in = client.getInputStream();
        //开始接受服务器的响应
        byte[]re_bytes = new byte[1024];
        while((len=in.read(re_bytes))!=-1){
            System.out.println(new String(re_bytes,0,len));
        }
        //4、注意本地的读取流需要关闭
        fileinput.close();
        //断开客户端和服务器的连接。
        client.close();


    }
}

2、服务器的创建

/**
 * 上传文件到服务器,服务器告知客户上传成功!
 *
 */
public class Practice3_TCP_FileServer {
    public static void main(String[]args) throws IOException {
        System.out.println("服务器启动!");
        //1、服务器必须明确自己是哪一个端口。
        ServerSocket server = new ServerSocket(10111);

        //2、判断是否有客户连接
        Socket client = server.accept();//成功返回socket对象说明有客户连接。
        System.out.println(client.getInetAddress()+"客户连接。");
        //3、获取socket流,进行接受数据并给予反馈
        InputStream in = client.getInputStream();
        OutputStream out = client.getOutputStream();

        //本地写入流
        FileOutputStream fileOut = new FileOutputStream(new File("E:\\socket_upload.JPG"));

        //4、接受数据、使劲往里边写
        byte[]bytes = new byte[1024*1024];//1M大小的内存
        int len=0;
        while((len=in.read(bytes))!=-1){
            fileOut.write(bytes,0,len);
        }

        //5、接受完毕需要给客户端发送上传完毕的反馈。
        out.write("UpLoad successfully!".getBytes());

        fileOut.close();//本地流关
        client.close();//客户端关
        //服务器一般不关
    }
}

选择的文件:
在这里插入图片描述

3、开始上传
先启动服务器:
在这里插入图片描述
启动客户端开始上传文件:
由于上传太快,一运行就收到上传成功。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.3.5、客户连接服务器容易出现的细节问题——线程阻塞状态

  • read()和readLine()等阻塞式方法,在接受客户端或者服务器等的发送的数据,一定要注意是否能终止,而不出现服务器和客户端都在等待的现象
  • 使用字符流来发送和接受数据时,是否添加了刷新?有些字节流不需要刷新。如果要刷新的不加刷新,会导致阻塞,进而出现服务器和客户端都在等待的现象,此时数据发送接受异常
  • 注意什么时候使用socket.shutdownOutput(),socket.shutDownInput()

阻塞通信(我们必须避免
1、客户端
在这里插入图片描述
2、服务器端
在这里插入图片描述
在这里插入图片描述
我们要非常注意这个阻塞状态的发送,它可能导致程序无法达到我们预期的功能。

我们要做到非阻塞通信!

2.4、深入剖析Socket用法♥

2.4.1 客户端连接服务器可能抛出的异常

在这里插入图片描述

2.4.2 关闭Socket类

毫无疑问,Socket调用到了底层资源,如网卡,端口等。我们不用的时候就该关闭。

socket.close();//尽量将其放在fianlly快里边,保证一定被执行。

状态测试:
在这里插入图片描述

2.4.3 半关闭Socket

在这里插入图片描述
就是在发送数据或者读取发过来的数据的时候,告知对方发送完毕,或者接受完毕。进而需要使用一个标识,做法可以是:

  • 使用时间戳标识
  • 使用某个字符串(不建议、因为文件或者发送消息可能包含该字符串)、
  • 使用内置的方法:shutdownInput()或者shutdownOutput()方法。

2.4.4 设置Socket选项

在这里插入图片描述

  • 1、void setTcpNoDelay(boolean on)
    启用/禁用 TCP_NODELAY(禁用/启用Nagle的算法)。 在这里插入图片描述
  • 2、void setReuseAddress(boolean on)
    启用/禁用 SO_REUSEADDR套接字选项。
    使用Socket.close()方法关闭Socket,并不会立即释放该端口,而是等待一段时间,如果此时有数据发送过来,Socket会接受而不作任何处理。防止因立即释放端口,而导致数据被其他占用该端口的程序接受在这里插入图片描述
  • 3、void setSoTimeout(int timeout)
    启用/禁用 SO_TIMEOUT以指定的超时时间,以毫秒为单位。
    我们知道Socket的输入流读取数据是阻塞式的(read,readLine),当没有数据过来,会一直等待。我们可以设置限制的等待时间。(可以和Thread.sleep()搭配观察效果)在这里插入图片描述
  • 4、void setSoLinger(boolean on, int linger)
    启用/禁用 SO_LINGER与指定的逗留的时间秒。 在这里插入图片描述
  • 5、void setSendBufferSize(int size)
    设置这个 Socket指定值的 SO_SNDBUF选项。在这里插入图片描述
  • 6、void setReceiveBufferSize(int size)
    集 SO_RCVBUF选项,这 Socket指定值。 在这里插入图片描述
  • 7、void setKeepAlive(boolean on)
    启用/禁用 SO_KEEPALIVE。 在这里插入图片描述
  • 8、void setOOBInline(boolean on)
    启用/禁用 SO_OOBINLINE(TCP紧急数据收据)默认情况下,此选项是禁用TCP套接字上接收紧急数据是默默丢弃。在这里插入图片描述
    9、public void setTrafficClass(int TrafficClass)
    设置服务类型 在这里插入图片描述
    10、public void setPerformances(int connectionTime,int latency,int bandwidth)
    设置带宽、连接时间和延迟
    在这里插入图片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章