参考资料:百度百科TCP协议

本文涉及Java IO流、异常的知识,可参考我的另外的博客

一文简述Java IO

一文简述JAVA内部类和异常

1.概述

计算机网络相关知识:

OSI七层模型

一个报文可以类似于一封信,就像下图(引自狂神说Java)非常生动。

网络编程的目的:数据交换、通信

网络通信的要素:

如何实现网络通信?

通信双方地址:

  • ip
  • 端口号

网络协议:

HTTP, FTP, TCP, UDP 等等

1.1 IP

IP地址:InetAddress(无构造器)

  • 唯一定位一台网络上计算机
  • 127.0.0.1 :本机,localhost
  • ip地址分类:ipv4(4个字节)/ipv6(128位,8个无符号整数组成),公网(ABCD类地址)/私网(局域网)
  • 域名:记忆ip问题

主机名解析

主机名称到IP地址解析是通过使用本地机器配置信息和网络命名服务(如域名系统(DNS)和网络信息服务(NIS))的组合来实现的。所使用的特定命名服务是默认配置的本地机器。对于任何主机名,返回其对应的IP地址。反向名称解析意味着对于任何IP地址,返回与IP地址关联的主机。

InetAddress类提供了将主机名解析为其IP地址的方法,反之亦然。

InetAddress常用方法:

Java网络编程快速上手(SE基础)-LMLPHP

举例:

public static void main(String[] args) throws UnknownHostException {
    //查询本机地址
    InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
    System.out.println(inetAddress);

    InetAddress i1 = InetAddress.getByName("localhost");
    System.out.println(i1);

    InetAddress i2 = InetAddress.getLocalHost();
    System.out.println(i2);

    //查询网站ip
    InetAddress i3 = InetAddress.getByName("www.baidu.com");
    System.out.println(i3);
    //常用方法
    System.out.println(i3.getAddress());   //返回的是byte[],所以输出了乱码
    System.out.println(i3.getCanonicalHostName());//规范的名字
    System.out.println(i3.getHostAddress());//ip
    System.out.println(i3.getHostName());//主机名
}

InetAddress没有构造器,所以需要调用静态方法进行构造。上述代码结果为:

1.2 端口

端口表示计算机上的一个程序的进程

  • 不同的进程有不同的端口号,用来区分软件。
  • 一般被规定为0~65535
  • TCP端口和UDP端口,均有65536个,两个互不冲突。单个协议下端口是不能冲突的,例:TCP占用8080后,不能再次占用此TCP端口了
  • 端口分类:公有端口049151,用来分配给用户或者程序,Tomcat:8080,MySQL:3306,Oracle:1521。动态、私有:49152~65535,尽量不要用这里的端口。
netstat -ano  #这条命令用于查看所有端口
netstat -ano|findstr "8080"   #查看指定的端口
tasklist|findstr "8696" #查看指定端口的进程

以上均为Linux命令

InetSocketAddress

该类实现IP套接字地址(IP地址+端口号)它也可以是一对(主机名+端口号),在这种情况下将尝试解析主机名。如果解决方案失败,那么该地址被认为是未解决的,但在某些情况下仍可以使用,例如通过代理连接。

它提供了用于绑定,连接或返回值的套接字所使用的不可变对象。

通配符是一个特殊的本地IP地址。 通常意味着“任何”,只能用于bind操作。

InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8080);
InetSocketAddress inetSocketAddress1 = new InetSocketAddress("localhost",8080);
System.out.println(inetSocketAddress);
System.out.println(inetSocketAddress1);
System.out.println(inetSocketAddress.getAddress());
System.out.println(inetSocketAddress.getHostName());
System.out.println(inetSocketAddress.getPort());

以上为相关代码。

1.3 通信协议

网络通信协议可能涉及到:速率,传输码率,代码结构,传输控制等等

主要涉及的是以下两个:

TCP:用户传输协议(3次握手,确定返回信息,以后网络相关知识具体说,不在本篇赘述)

UDP:用户数据报协议(不确定返回信息)

TCP和UDP对比

TCP就像打电话,需要连接,稳定

UDP就像发短信,不需要连接,发完即结束,不稳定

  • 基于连接与无连接;

  • 对系统资源的要求(TCP较多,UDP少);

  • UDP程序结构较简单;

  • 流模式与数据报模式 ;(从下面demo中即可看出)

  • TCP保证数据正确性,UDP可能丢包;

  • TCP保证数据顺序,UDP不保证。

2. TCP协议

TCP三次握手的过程如下:

  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。

三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。

TCP连接终止过程:

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如上图所示。

(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。

(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]

既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

TCP协议相关资料参考自百度百科,计算机网络相关知识不再详细描述。

2.1 TCP连接的实现

服务端:

public class TestTCPserver {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        Socket accept = serverSocket.accept();//等待client连接
        InputStream is = accept.getInputStream();
        //管道流,将一个输入流通过管道转化为一个合适的输出流,不用管道流直接String可能会输出乱码
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while((len=is.read(buffer))!=-1){
            baos.write(buffer,0,len);
        }
        System.out.println(baos.toString());
        baos.close();
        is.close();
        accept.close();
        serverSocket.close();
        //正式写代码的过程中,一定要用try,catch,finally,为了代码的安全,出了事故容易判断
    }
}

用到了socket类,其中涉及了IO流部分的知识。

客户端:

public class TestTCPclient {
    public static void main(String[] args) throws IOException {
        InetAddress serverIp = InetAddress.getByName("127.0.0.1");
        int port = 9999;
        //创建一个socket连接,连接的是本机
        Socket socket = new Socket(serverIp,port);
        OutputStream os = socket.getOutputStream();
        os.write("hello".getBytes());
        os.close();
        socket.close();
    }
}

注意:socket是需要关闭的

  • socket该类实现客户端套接字(也称为“套接字”)。套接字是两台机器之间通讯的端点。套接字的实际工作由SocketImpl类的实例执行。 应用程序通过更改创建套接字实现的套接字工厂,可以配置自己创建适合本地防火墙的套接字。

  • ServerSocket该类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
    服务器套接字的实际工作由SocketImpl类的实例执行。 应用程序可以更改创建套接字实现的套接字工厂,以配置自己创建适合本地防火墙的套接字。

客户端:连接服务器socket,发送消息

服务器:建立服务的端口 ServerSocket,等待用户连接,接受用户消息

2.1 TCP实现文件上传

与消息传递类似,只是IO操作稍微变了一下

服务端:

public class TCPserverdemo1 {
    public static void main(String[] args) throws IOException {
        //创建服务
        ServerSocket serverSocket = new ServerSocket(9000);
        //监听客户端的连接
        Socket accept = serverSocket.accept();  //阻塞式监听,会一定等待客户端连接
        InputStream is = accept.getInputStream();
        FileOutputStream fos = new FileOutputStream(new File("copide1.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        fos.close();
        is.close();
        accept.close();
        serverSocket.close();
    }
}

客户端:

public class TCPclientdemo1 {
    public static void main(String[] args) throws IOException {
        //创建一个socket连接
        Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9000);
        //创建一个输出流
        OutputStream os = socket.getOutputStream();  //.getOutputStream获得了一个SocketOutputStream实例
        //读取文件
        FileInputStream fis = new FileInputStream(new File("image.jpg"));
        byte[] buffer = new byte[1024];
        int len;
        while((len=fis.read(buffer))!=-1){
            os.write(buffer,0,len);    //涉及到BIO
        }
        fis.close();
        os.close();
        socket.close();
    }
}

上面用的是字节流,字节缓冲流也可以使用。字符流不可以,因为可能会读其他文件的类型,字节流比较稳妥。

服务器接收完信息后其实是可以返回消息到客户端的,socket通信:

服务端可增加:

accept.shutdownInput();
//通知客户端已经接收完毕
OutputStream os = accept.getOutputStream();
os.write("ending".getBytes());    //发送给客户端

客户端可增加:

socket.shutdownOutput();//通知服务器传输完毕
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while((len2 = is.read(buffer))!=-1){
    baos.write(buffer,0,len2);
}
System.out.println(baos);

客户端读入了服务端返回的ending字符串。

实现两端通信之后再决定是否进行其他操作,例如close()等等。

3. UDP

无需连接,但是需要知道对方地址

3.1 UDP消息发送

主要依赖的是DatagramSocketDatagramPacket

  • DatagramSocket此类表示用于发送和接收数据报数据包的套接字。 数据报套接字是分组传送服务的发送或接收点。 在数据报套接字上发送或接收的每个数据包都被单独寻址和路由。 从一个机器发送到另一个机器的多个分组可以不同地路由,并且可以以任何顺序到达。 在可能的情况下,新构建的DatagramSocket启用了SO_BROADCAST套接字选项,以允许广播数据报的传输。 为了接收广播数据包,DatagramSocket应该绑定到通配符地址。 在一些实现中,当DatagramSocket绑定到更具体的地址时,也可以接收广播分组。
  • DatagramPacket该类表示数据报包。 数据报包用于实现无连接分组传送服务。 仅基于该数据包中包含的信息,每个消息从一台机器路由到另一台机器。 从一台机器发送到另一台机器的多个分组可能会有不同的路由,并且可能以任何顺序到达。 包传送不能保证。

以下实现UDP消息传送的一个简单例子:

public class TestUDP1 {
    //不需要连接服务器
    public static void main(String[] args) throws IOException {
        //建立Socket
        DatagramSocket datagramSocket = new DatagramSocket(); //为空将默认绑定一个可用端口

        //建个数据报
        String message = "hello,server";
        InetAddress inetAddress = InetAddress.getByName("localhost");
        int port = 9000;
        int len = message.getBytes().length;
        //数据,数据的长度起始位置,要发送给谁
        DatagramPacket datagramPacket = new DatagramPacket(message.getBytes(), 0,len, inetAddress, port);

        datagramSocket.send(datagramPacket);   //进行发送
        datagramSocket.close();
    }
}

UDP发送完就不需要其他操作了,为了验证我们发送的消息,建立了一个接收端:

public class UDPaccept {
    public static void main(String[] args) throws IOException {
        //开放端口
        DatagramSocket socket = new DatagramSocket(9000);
        //接收数据报
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length); //接收并不需要对方地址和端口
        socket.receive(packet);   //阻塞式接收
        System.out.println(packet.getAddress());
        System.out.println(new String(packet.getData(),0,packet.getData().length));
        socket.close();
    }
}

其中收发消息用到了两个方法DatagramSocket.send()DatagramSocket.receive()

3.2 UDP聊天的实现(单向)

通过UDP协议进行一个发送端和接收端的demo。当出现"bye"时,结束对话。

发送端:

public class UDPsender {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(8080);//发送端口
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        //BufferedInputStream bis = new BufferedInputStream(System.in);
        int len;
        while(true){
            String data = reader.readLine();      //接收键盘输入的信息
            DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress("localhost",6000));
            socket.send(packet);
            if(data.equals("bye"))
                break;
        }
        reader.close();
        socket.close();
    }
}

接收端:

public class UDPreceiver {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(6000);//打开接收端口

        while(true){
            byte[] container = new byte[1024]; //用来装数据报内容
            DatagramPacket packet = new DatagramPacket(container,0,container.length);   //接受时候只需要一个空byte[]
            socket.receive(packet);
            byte[]data = packet.getData();
            String datas = new String(data,0,data.length);  //仍带有byte[]的其他信息,若转为真正字符串需trim()
            System.out.println(datas);
            if(datas.trim().equals("bye"))
                break;
        }
        socket.close();
    }
}

这个demo只实现了单向发消息。后续将实现双向发送。

3.3 双向聊天(多线程)

双向聊天和以上内容相似,只需要每个端开启两个线程(接收线程和发送线程),以下为代码演示:

public class TalkSend implements Runnable{    //发送线程
    DatagramSocket socket = null;
    BufferedReader reader = null;

    private String ToIP;
    private int ToPort;

    public TalkSend(String toString, int toPort) {
        ToIP = toString;
        ToPort = toPort;
    }

    @Override
    public void run() {
        try {
            socket = new DatagramSocket();//发送端口
            reader = new BufferedReader(new InputStreamReader(System.in));
        } catch (SocketException e) {
            e.printStackTrace();
        }
        //BufferedInputStream bis = new BufferedInputStream(System.in);
        while(true){
            String data = null;
            try {
                data = reader.readLine();
                DatagramPacket packet = new DatagramPacket(data.getBytes(),0,data.getBytes().length,new InetSocketAddress(this.ToIP,this.ToPort));
                socket.send(packet);
                if(data.equals("bye"))
                    break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        socket.close();
    }
}
public class TalkReceive implements Runnable{   //接收线程
    DatagramSocket socket = null;

    private int FromPort;
    private String Person;

    public TalkReceive(int fromPort,String person) {
        FromPort = fromPort;
        Person = person;
    }

    @Override
    public void run() {
        try {
            socket = new DatagramSocket(this.FromPort);//打开接收端口
        } catch (SocketException e) {
            e.printStackTrace();
        }

        while(true){
            byte[] container = new byte[1024]; //用来装数据报内容
            DatagramPacket packet = new DatagramPacket(container,0,container.length);
            try {
                socket.receive(packet);
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[]data = packet.getData();
            String datas = new String(data,0,data.length);
            System.out.println(Person+":"+datas);
            if(datas.trim().equals("bye"))
                break;
        }
        socket.close();
    }
}

然后需要设置两个端进行聊天:

new Thread(new TalkSend("localhost",8080)).start();
new Thread(new TalkReceive(6000,"老师")).start();

这里设置为学生端,然后开启两个线程,设置发送端口和接收端口

new Thread(new TalkSend("localhost",6000)).start();
new Thread(new TalkReceive(8080,"学生")).start();

这里设置为老师端,然后开启线程,设置端口,其实打开了4个端口,因为学生端和老师端发送线程中,DatagramSocket默认绑定的还有两个端口。

4. URL

https://www.baidu.com/

统一资源定位符:定位互联网上的某个资源

DNS域名解析: www.baidu.com ——》xxx.xxx.xxx.xxx

协议://ip地址:端口/项目名/资源

URL类的基本使用

URL url = new URL("http://localhost:8080/helloworld/index.jsp?username=yuan&password=123");
System.out.println(url.getProtocol());//得到协议名
System.out.println(url.getHost());//主机
System.out.println(url.getPort());//端口
System.out.println(url.getPath());//文件
System.out.println(url.getFile());//文件全路径
System.out.println(url.getQuery());   //得到url查询的部分(参数)

Java万物皆对象

下载一个URL资源

public class UrlDown {
    public static void main(String[] args) throws IOException {
        URL url = new URL("http://localhost:8080/gaoyuan/SecurityFile.txt");
        //连接到这个资源
        HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); //需要转换类型,因为返回的是URPConnection
        InputStream is = urlConnection.getInputStream();
        FileOutputStream fos = new FileOutputStream("SecurityFile.txt");
        byte[] buffer = new byte[1024];
        int len;
        while((len = is.read(buffer))!=-1){
            fos.write(buffer,0,len);
        }
        fos.close();
        is.close();
        urlConnection.disconnect();//断开连接
    }
}

这是打开Tomcat服务器后,把一个文件添加到相应目录之后下载的。

网络编程基础部分结束,例如:计算机网络,BIO等知识将会在后续博客中发布。

04-18 05:32