网络编程,Socket的使用,服务器和客户端,TCP和UDP(二十二)

522 阅读13分钟

网络基本概念

1、应用程序有两种结构:C/S和B/S

C:Client客户端

S:Server服务器

B:Browser浏览器

C/S和B/S的区别?

C/S:需要开发两套程序,一套是客户端程序,一套是服务器端程序,用户这边需要在它的电脑安装我们客户端程序。

B/S: 用户统一使用浏览器访问我们的服务器端程序。

其实本质上B/S也是C/S结构,客户端是由浏览器公司帮我们开发了。

2、网络的通信需要哪些要素?

(1)IP地址:收发地址

(2)端口号:电脑中通过端口号来识别应用程序

相当于 姓名:收发人姓名

(3)网络协议

内容:要求双方看得懂

3、IP地址

​ 互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。

分类1:IPV4,IPV6

分类2:A,B,C...根据范围来分。

特殊的IP地址:127.0.0.1 本地回环地址,它对应的域名:localhost

一会儿大家要写网络编程的应用程序了,但是你是在同一台电脑上写了客户端服务器端,

此时需要网络通信时,地址怎么表示?表示自己给自己发,可以用127.0.0.1。

4、域名:方便用户记忆和使用

域名对应一个IP地址

www.atguigu.com ==> 202.108.35.210

域名->IP,需要DNS,域名解析器

5、端口号:作用是唯一定位到一个进程(运行中的应用程序)

是一个整数,范围是0-65535

分为:

公认端口:0~1023

注册端口:1024~49151 mysql:3306,tomcat:8080

动态/私有端口:49152~65535。

6、网络协议

​ 在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。

一开始是7层,后来实际是4层。

其中传输层有两大经典协议:

TCP:传输控制协议 (Transmission Control Protocol)

面向连接的、可靠的、基于字节流的传输协议

什么叫做面向连接的?

在传输数据之前,需要先建立连接,并且要确认这个连接是没问题,才会开始传输数据。

尝试连接并确认连接这个过程一般需要经过“三次握手”。

在传输数据之后,需要断开连接,并且要保证数据传输完成后再彻底断开。

断开连接过程一般需要经过“四次挥手”。

什么叫做可靠的?

​ 使用TCP协议发生数据的话,数据会拆为好几个包,这些包会陆陆续续到达目的地。并且这些数据包可能不能经过相同的路径到达目的地。目的地这边,会对这些数据包进行重组。重组时,会按照编号进行处理,但是如果发现某个编号的数据包丢了,先等一会儿,一会儿还没有,就会让发送方重传。

优点:可靠,适用于传输大量数据

UDP:用户数据报协议(User Datagram Protocol)

​ 非面向连接的,不可靠的、基于数据报的传输协议

​ 基于UDP协议传输数据的话,在传输数据之前,不会确认接收方是否存在(是否能联通),直接发送。如果对方没收到,也不会主动重传。

​ 优点:快,只适用于传输小的数据,每一个数据报容量64K以内。

InetAddress类

​ InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。

Internet上的主机有两种方式表示地址:

  • 域名(hostName):www.atguigu.com
  • IP 地址(hostAddress):202.108.35.210

lInetAddress 类没有提供公共的构造器,而是提供 了 如下几个 静态方法来获取InetAddress 实例

  • public static InetAddress getLocalHost()
  • public static InetAddress getByName(String host)
  • public static InetAddress getByAddress(byte[] addr)

InetAddress 提供了如下几个常用的方法

  • public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)。
  • public String getHostName() :获取此 IP 地址的主机名

Socket类

​ Socket是代表网络通信的一方。双方都有各自的Socket。

Socket可以分为:

(1)流套接字:ServerSocket和Socket类,用于TCP协议编程。

​ ServerSocket:负责在服务器端,接收客户端的连接用的。即它不负责传输数据。

​ Socket类:负责传输数据。

(2)数据报套接字:DatagramSocket,用于UDP协议编程。

DatagramSocket:发送方和接收方都有DatagramSocket的对象。Socket是负责与网卡驱动交互。

ServerSocket类的构造方法:

  • ServerSocket(int port) :创建绑定到特定端口的服务器套接字。

ServerSocket类的常用方法:

  • Socket accept():侦听并接受到此套接字的连接。

Socket类的常用构造方法

  • public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
  • public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号。

Socket类的常用方法

  • public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息
  • public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送消息
  • public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
  • public InetAddress getLocalAddress():获取套接字绑定的本地地址。
  • public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
  • public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。
  • public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
  • public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
  • public void shutdownOutput():禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。

**注意:**先后调用Socket的shutdownInput()和shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等于调用Socket的close()方法。在通信结束后,仍然要调用Scoket的close()方法,因为只有该方法才会释放Socket占用的资源,比如占用的本地端口号等。

​ 基于UDP协议的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。

DatagramSocket 类的常用方法:

  • public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
  • public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。
  • public void close()关闭此数据报套接字。
  • public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
  • public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。

DatagramPacket类的常用方法:

  • public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。
  • public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。
  • public int getLength()返回将要发送或接收到的数据的长度。

TCP编程示例1

服务器

/*
客户端与服务器端连接成功后,客户端给服务器发一个消息:“hello"
然后服务器回复客户端一个"hi"

服务器端程序与客户端程序谁先运行?服务器端
 */
public class TestServer {
    public static void main(String[] args) throws IOException {
        //(1)开启服务器,准备一个ServerSocket对象
        //有一个参数必须指定,你服务器监听的端口号
        //一会儿客户端要连接服务器,必须指定服务器的端口号
        ServerSocket server = new ServerSocket(8888);

        //(2)接受客户端的连接
        //accept()方法是一个阻塞式方法,即如果没有客户端连接,这个方法不往下走
        //一旦有客户端连接,这个方法就会提供一个Socket对象,负责与该客户端通信
        Socket socket = server.accept();
        System.out.println(socket.getInetAddress().getHostAddress() +"连接成功!");

        //(3)如果要支持同时收和发,就需要多线程
        //如果不是多线程,就要规定,是先收还是先发
        //例如:先收
        InputStream inputStream = socket.getInputStream();
        byte[] data = new byte[10];
        int len;
        while((len = inputStream.read(data))!=-1){
            System.out.println(new String(data, 0, len));
        }

        //回复消息
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hi".getBytes());

        //(4)如果通信结束,可以断开连接,释放对应的资源
        outputStream.close();
        inputStream.close();
        socket.close();
        server.close();
    }
}

客户端

public class TestClient {
    public static void main(String[] args) throws IOException {
        //(1)主动连接服务器,准备一个Socket对象
        //至少指定两个参数:服务器的IP地址,和服务器监听的端口号
        Socket socket = new Socket("127.0.0.1",8888);

        //(2)如果要支持同时收和发,就需要多线程
        //如果不是多线程,就要规定,是先收还是先发
        // 例如:先发
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello".getBytes());
        socket.shutdownOutput();//告诉服务器我不再发消息了,对方才能读到流末尾标记-1

        //收消息
        InputStream inputStream = socket.getInputStream();
        byte[] data = new byte[10];
        int len;
        while((len = inputStream.read(data))!=-1){
            System.out.println(new String(data, 0, len));
        }

        //(3)如果通信结束,可以断开连接,释放对应的资源
        outputStream.close();
        inputStream.close();
        socket.close();
    }
}

UDP编程实例

发送方

/*
基于UDP协议的编程。
发送方:“十一假期马上来了"
接收方:收到后打印
 */
public class TestSend {
    public static void main(String[] args) throws Exception {

        //(1)准备DatagramSocket
        DatagramSocket ds = new DatagramSocket();

//        (2)把数据报装到数据报中
        //DatagramPacket(byte[] buf, int length, InetAddress address, int port)
        //参数一:字节数组,参数二:长度,参数三:接收方的IP地址,参数四:接收方的端口号
        String message = "十一假期马上来了";
        byte[] data = message.getBytes();
        byte[] ip = {(byte)192,(byte)168,40,116};
        InetAddress address = InetAddress.getByAddress(ip);
        DatagramPacket dp1 = new DatagramPacket(data, data.length, address, 8888);
        DatagramPacket dp2 = new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9999);

        //(3)发送数据报
        ds.send(dp1);
        ds.send(dp2);
        System.out.println("发送完毕");

        //(4)如果接收,关闭
        ds.close();
    }
}

接受方

public class TestReceive {
    public static void main(String[] args)throws Exception {
        //(1)准备DatagramSocket,必须指定监听端口号
        DatagramSocket ds = new DatagramSocket(9999);

        //(2)准备数据报来接收消息
        //DatagramPacket(byte[] buf, int length)
        byte[] data = new byte[1024];
        DatagramPacket dp = new DatagramPacket(data, data.length);

        //(3)接收数据报
        ds.receive(dp);

        //(4)处理数据
        System.out.println(new String(dp.getData(),0,dp.getLength()));

        //(5)关闭
        ds.close();

    }
}

TCP编程示例2

服务器

public class TestServer {
    public static void main(String[] args) throws Exception{
        //(1)开启服务器
        ServerSocket server = new ServerSocket(8888);

        //(2)接收多个客户端
        while(true) {
            Socket socket = server.accept();//一句这个代码可以接收一个客户端的连接
            //这句代码每运行一次,就表示接收一个客户端
            System.out.println(socket.getInetAddress().getHostAddress()+"连接成功");

            //每一个客户端都有单独的线程为它服务
            new MyThread(socket).start();
        }
    }
}

class MyThread extends Thread{
    private Socket socket;

    public MyThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run(){
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            InputStreamReader isr = new InputStreamReader(inputStream);
            BufferedReader br = new BufferedReader(isr);
            PrintStream ps = new PrintStream(outputStream);
            ){
            //服务器接收消息,并且返回给客户端
            /*byte[] data = new byte[1024];
            int len;
            while((len = inputStream.read(data)) != -1){

            }*///不好处理每个单词的分界
            //处理方式改为:按行读取,对方(客户端)每输入一个单词回车结束,发送过来一个单词一行
             //返回给客户端也按行处理
            String line;
            while((line = br.readLine()) != null){
                System.out.println("客户端" + socket.getInetAddress().getHostAddress() + "发送的单词是:" + line);

                //把单词反转并返回
                ps.println(new StringBuilder(line).reverse());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

服务器

public class TestClient {
    public static void main(String[] args)throws Exception {
        //(1)连接服务器
        Socket socket = new Socket("127.0.0.1",8888);

        OutputStream outputStream = socket.getOutputStream();
        PrintStream ps = new PrintStream(outputStream);

        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader br = new BufferedReader(isr);

        //(2)从键盘输入单词或成语等,非服务器发过去
        Scanner input = new Scanner(System.in);
        while(true){
            System.out.print("请输入单词或成语:");
            String word = input.nextLine();
            if("stop".equals(word)){
                break;
            }
            ps.println(word);

            //接收服务器返回的消息
            System.out.println("服务器返回:" + br.readLine());
        }

        //(3)关闭
        ps.close();
        br.close();
        isr.close();
        inputStream.close();
        outputStream.close();
        socket.close();
    }
}

TCP编程示例3

客户端

/*
案例:从客户端给服务器上传文件,可以支持多个客户端同时上传。
    服务器接收完文件之后,返回“xx文件接收完毕”

 */
public class TestFileServer {
    public static void main(String[] args)throws Exception {
        //(1)开启服务器
        ServerSocket server = new ServerSocket(8888);

        //(2)同时支持多个客户端
        while(true){
            Socket socket = server.accept();
            System.out.println(socket.getInetAddress().getHostAddress()+"连接成功!");

            //每一个客户端一个线程
            new FileUploadThread(socket).start();
        }
    }
}
class FileUploadThread extends Thread{
    private Socket socket;

    public FileUploadThread(Socket socket) {
        this.socket = socket;
    }

    public void run(){
        FileOutputStream fos = null;
        BufferedOutputStream bos = null;
        try(
                InputStream inputStream = socket.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(inputStream);
                //文件名是字符串,文件内容不一定是字符串,肯定不能用字符流
                //如果使用纯字节流,需要 区别是文件名还是文件内容,即知道文件名一共多少个字节?很难办
                //如何解决?DataInputStream或ObjectInputStream
                DataInputStream dis = new DataInputStream(bis);

                OutputStream outputStream = socket.getOutputStream();
                PrintStream ps = new PrintStream(outputStream);


        ){
            //接收文件名
            String fileName = dis.readUTF();
            //处理文件名
            //如果服务器端原样存储文件的话,可能有文件“重名”问题,怎么办?
            //解决方案有很多种,其中一种是  用时间戳+ip地址+文件的后缀名
            long time = System.currentTimeMillis();
            String ip = socket.getInetAddress().getHostAddress().replaceAll("\\.","");
            String ext = fileName.substring(fileName.lastIndexOf("."));
            String newFileName = time + "_" + ip + ext;

            fos = new FileOutputStream("upload/" + newFileName);
            bos = new BufferedOutputStream(fos);

            //接收文件内容
            byte[] data = new byte[1024];
            int len;
            while((len = dis.read(data)) != -1){
                bos.write(data, 0, len);
            }

            //反馈接收完毕
            ps.println(fileName + "接收完毕!");

        }catch(Exception e){
            e.printStackTrace();
        }finally{
            //手动关闭
            try {
                if(bos!=null){
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fos!=null){
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务器

public class TestFileClient {
    public static void main(String[] args) throws Exception{
        //(1)连接服务器
        Socket socket = new Socket("127.0.0.1",8888);

        //(2)指定上传的文件
        Scanner input = new Scanner(System.in);
        System.out.print("请输入要上传的文件的路径名:");
        String filePathName = input.nextLine();
        //D:\Download\img\美女\15.jpg
        File file = new File(filePathName);

        //(3)发送文件名和文件的内容
        OutputStream outputStream = socket.getOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(outputStream);
        DataOutputStream dos = new DataOutputStream(bos);

        FileInputStream fis = new FileInputStream(filePathName);
        BufferedInputStream bis = new BufferedInputStream(fis);

        //发送文件名
        dos.writeUTF(file.getName());
        //发送文件内容
        byte[] data = new byte[1024];
        int len;
        while((len = bis.read(data)) != -1){
            dos.write(data, 0 ,len);
        }
        dos.flush();//把缓冲区中的数据先刷出去,才能关闭输出通道
        socket.shutdownOutput();//告诉服务器,文件内容传输完毕

        //接收服务器反馈的消息
        InputStream inputStream = socket.getInputStream();
        InputStreamReader isr = new InputStreamReader(inputStream);
        BufferedReader br = new BufferedReader(isr);

        System.out.println(br.readLine());

        //各种关闭
        br.close();
        isr.close();
        inputStream.close();

        bis.close();
        fis.close();

        dos.close();
        bos.close();
        outputStream.close();

        socket.close();
    }
}