Java网络编程(基于TCP和UDP的通信实现)

简单说一些常见的协议所属的OIS层:
应用层:远程登录协议Telnet、文件传输协议FTP,超文本传输协议HTTP,域名服务DNS,简单邮件传输协议SMTP,邮局协议POP3等
传输层:传输控制协议TCP—-面向连接的可靠传输协议,用户数据报协议UDP—–是无连接的,不可靠的传输协议
网络层:网际协议IP,Internet互联网控制报文协议ICMP、Internet组管理协议IGMP

TCP:Transfer Control Protocol面向连接的可靠传输协议
发送方和接收方需要建立连接。双方之间的通信通过socket
一方的socket(通常是Server Socket)等待建立连接,另一方的socket可以要求进行连接。一旦两个socket连接起来,双方就可以进行双向的数据传输,双方都可以进行发送和接受数据的操作
传输的数据大小没有限制

HTTP、FTP、Telnet等应用都需要这种可靠的通信通道

UDP:User Datagram Protocol用户数据报协议。
无连接的协议,每个数据包都是一个独立的信息,包括完整的源地址或目的地址。
在网络上以任何可能得路径传往目的地,因此是否到达目的地,到达目的地的时间及内容的正确性都是不能被保证的;发送的内容可能无序
传输数据的大小有限制,每个被传输的数据报必须限定在64KB之内

IP:用来标识网络上的计算机
端口号:用来指明该计算机的应用程序。16位数字表示,范围是0~65535,1024以下的端口号保留给预定义的服务。如,http使用80端口
这里写图片描述
Java中,通过java.net包中的类,Java程序能够使用TCP或UDP协议在互联网上进行通讯

URL:Uniform Reosurce Locator统一资源定位符,表示Internet上某一资源的地址,通过URL我们可以访问Internet上的各种网络资源
URL:协议标识符—-HTTP,FTP,File+资源名字—主机名,文件名,端口号,引用
eg:http://java.su.com:80/docs/books/tutorial/index.html#DOWN

Java中,可以创建表示URL地址的URL对象。
通过该URL对象,可以获得URL对象的各个属性及获得URL的实际比特或内容消息。

/**
     getDefaultPort(): 返回默认的端口号。
   getFile(): 获得URL指定资源的完整文件名。
   getHost(): 返回主机名。
   getPath(): 返回指定资源的路径。
   getPort(): 返回端口号,默认为-1。
   getProtocol(): 返回表示URL中协议的字符串对象。
   getRef(): 返回URL中的HTML文档标记,即#号标记。
   getUserInfo: 返回用户信息。
   toString: 返回完整的URL字符串。
 */
public class UrlTest1 {
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("http://java.su.com:80/docs/books/tutorial/index.html#DOWN");
        System.out.println("URL 是 " + url.toString());
        System.out.println("协议是 " + url.getProtocol());
        System.out.println("文件名是 " + url.getFile());
        System.out.println("主机是 " + url.getHost());
        System.out.println("路径是 " + url.getPath());
        System.out.println("端口号是 " + url.getPort());
        System.out.println("默认端口号是 "+url.getDefaultPort());
        System.out.println("引用 "+url.getRef());
    }
}

输出结果:
URL 是 http://java.su.com:80/docs/books/tutorial/index.html#DOWN
协议是 http
文件名是 /docs/books/tutorial/index.html
主机是 java.su.com
路径是 /docs/books/tutorial/index.html
端口号是 80
默认端口号是 80
引用 DOWN

/**
 *通过URL对象获得URLConnection对象,代表应用程序和URL之间的通信连接,此类的实例可用于读取和写入此URL引用的资源
 */
public class UrlTest2 {
    public static void main(String[] args) throws IOException {
        URL url = new URL("https://www.baidu.com");
        //获得URLConnection对象
        URLConnection conn = url.openConnection();
        //获得读取该URL引用资源的字节输入流
        InputStream is = conn.getInputStream();
        /**
         * 也可以直接通过URL对象获得引用资源的读取通道---字节输入流
         * InputStream is = url.openStream();
         *  也可以利用缓冲字符流读取数据,更方便
         *  BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
         */
        FileOutputStream fos = new FileOutputStream("d:/aa.html");
        byte[] buffer = new byte[2048];
        int len;
        while(-1 != (len = is.read(buffer))){
            fos.write(buffer,0,len);
        }
        fos.close();
        is.close();
    }
}

Java中InetAddress类封装数字式的IP地址和该地址的域名。
InetAddress类没有明显的构造函数。生成该对象,运用一个可用的工厂方法。

/**
*均可能会抛出异常UnknownHostException
*1.返回象征本地主机的InetAddress对象
*2.返回一个传给它的主机名的InetAddress
*3.返回代表有一个特殊名称分解的所有地址的InetAddress类数组
/
InetAddress addr1 = InetAddress.getLocalHost();
InetAddress addr2 = InetAddress.getByName("www.baidu.com");
InetAddress[] addrs = InetAddress.getAllByName("www.sohu.com");

网络通信的实现依赖于套接字Socket
Socket:连接运行在网络上的两个程序间的双向通讯的端点

通俗理解来说,就好比两个人打电话,套接字就扮演了手机的角色,作为两个人可以打电话的工具或者说是媒介。

对于TCP

使用Socket进行网络通信的过程:
1.服务器将套接字绑定到特定的端口,监听客户端的连接请求
2.客户端根据服务器程序所在的主机名和端口号发出连接请求
3.如果一切正常,服务器接收连接请求,并获得一个新的绑定到不同端口地址的套接字。原来的套接字继续监听是否有其他客户端的请求
4.客户端和服务器端通过读写套接字进行通讯

Java中如何实现呢?

使用ServerSocket和Socket实现服务器端和客户端的Socket通信
这里写图片描述
Step:
1.建立Socket连接
2.获得输入/输出流
3.读/写数据
4.关闭输入/输出流
5.关闭socket

//服务端
public class TcpServer {
    public static void main(String[] args) throws IOException {
        //建立ServerSocket监听客户端的Socket连接请求
        ServerSocket ss = new ServerSocket(5000);
        //监听是否有客户端的请求,否则阻塞等待,不往下执行
        Socket s = ss.accept();
        //获得输入流,读取客户端发送的数据
        InputStream is = s.getInputStream();
        /*疑问:至今未解,为何当利用如下代码读取数据的时候,就无法实现向客户端发送数据呢?循环跳出也不会往下执行,这是为什么?如果有经过的大神,希望可以解答一下~
        byte[] buffer = new byte[1024];
        int len;
        while(-1 != (len = is.read(buffer))){
            String str = new String(buffer,0,len);
            System.out.println(str);
        }*/
        byte[] buffer = new byte[1024];
        int len = is.read(buffer);
        String str = new String(buffer,0,len);
        System.out.println(str);
        //获得输出流,向客户端发送数据
        OutputStream os = s.getOutputStream();
        os.write("hello client".getBytes());
        //关闭输入、输出流及Socket
        os.close();
        is.close();
        s.close();
    }
}
//客户端
public class TcpClient {
    public static void main(String[] args) throws IOException {
        //建立连接指定地址和端口的Socket
        Socket s = new Socket("10.170.34.89",5000);
        //获得输出流,向服务器端发送数据
        OutputStream os = s.getOutputStream();
        os.write("hello server".getBytes());
        //获得输入流,读取服务器端发来的数据
        InputStream is = s.getInputStream();
        byte[] buffer = new byte[1024];
        int len = is.read(buffer);
        String str = new String(buffer,0,len);
        System.out.println(str);
    }
}

上述只是一个客户端和服务端建立Socket连接并进行收发数据的小Demo,服务器先读客户端发来的数据,再向客户端发送数据;而客户端先向服务器端发送数据,再读取服务器端发来的数据。但是在实际应用中的客户端和服务端并不是这样的模型,应该是服务器端和客户端之间的数据传输是任意的,它们可以随时收发数据。一个线程是满足不了上述要求的,这就需要我们通过多个线程来是实现随时收发数据。对于服务端来说,有读取数据的线程,也有发送数据的线程,只要来一个Socket连接,那么我们就分别创建基于该Socket的一个读线程和一个写线程,分别去读取数据和发送数据。对于客户端来说,也是一样的,有读取数据的线程,也有发送数据的线程,通过开启读写数据的线程从而实现随时收发数据。
服务端:

public class ServerMain {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(4000);
        //服务端一直处于监听状态,只要有客户端发来Socket连接请求,就创建新的读写线程与该客户端进行通信
        while(true){  
            Socket socket = ss.accept();
            new ServerInputStreamThread(socket).start();
            new ServerOutputStreamThread(socket).start();
        }
    }
}
/**
 * 服务端的发送数据线程
 */
public class ServerOutputStreamThread extends Thread{
    private Socket socket;
    public ServerOutputStreamThread(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            OutputStream os = socket.getOutputStream();
            //一直处于发送数据的状态,只要有用户输入,就将数据发送
            while (true){
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                String str = br.readLine();
                os.write(str.getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/**
 * 服务端的读取数据线程
 */
public class ServerInputStreamThread extends Thread {
    private Socket socket;
    public ServerInputStreamThread(Socket socket){
        this.socket = socket;
    }
    public void run(){
        try {
            InputStream is = socket.getInputStream();
            //一直处于等待读取数据的状态,只要客户端有数据发来,就将数据读取
            while(true){
                byte[] buffer = new byte[1024];
                int len = is.read(buffer);
                String str = new String(buffer,0,len);
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述Demo在只有一个客户端的时候,服务器和客户端可以正常通信,但是当我们开启两个客户端的时候,两个客户端向服务器发送数据没有任何问题,因为每个客户端发起Socket连接请求的时候,都指定了IP和端口号。但是当服务器向客户端发送数据的时候,出现了问题,此时服务器发送的数据应该到哪个客户端呢?
这是因为我们都是在本地模拟的客户端和服务端,虽然开启了两个程序来模拟两个客户端,但是每个客户端向服务端发起请求的时候,服务端分别为“两个客户端”创建了Socket对象,由于实际上两个客户端的IP和端口号是一致的,所以在服务端向客户端发送消息的时候,由于两个Socket包含的客户端的信息一致,所以发送消息出现了异常。但是在实际应用场景中,客户端是在不同的计算机上,所以并不会出现这种收发消息异常的情况。因为每当有客户端发起Socket连接请求的时候,服务端都会创建一个Socket对象,并将该对象传入读写线程内,实现针对该客户端的读写操作。


对于UDP

同样也使用Socket,只不过UDP发送的数据报是网上传输的独立数据包,Java中使用DatagramPacket类来表示数据报对象,DatagramSocket类利用UDP协议来实现客户与服务器的Socket
send()发送数据报
receive()接收数据报
对于UDP协议来说,服务端和客户端的执行程序差别不是很大。

这里写图片描述
Step:
1.创建Socket对象
2.创建数据报DatagramPacket对象
3.读/写数据
4.关闭Socket对象

//视为客户端
public class UdpDemo1 {
    public static void main(String[] args) throws Exception {
        //创建DatagramSocket对象
        DatagramSocket socket = new DatagramSocket();
        String str = "hello world";
        //创建要发送的DatagramPacket数据报,其中包含要发往的目标的IP和端口号
        DatagramPacket packet = new DatagramPacket(str.getBytes(),str.length(), InetAddress.getByName("localhost"),4000);
        //发送数据报
        socket.send(packet);

        byte[] buffer = new byte[1024];
        //创建一个空的数据报用于接受消息
        DatagramPacket packet2 = new DatagramPacket(buffer,buffer.length);
        //接受数据报
        socket.receive(packet2);
        String recev = new String(packet2.getData(),0,packet2.getLength());
        System.out.println(recev);
        socket.close();
    }
}
//视为服务端
public class UdpDemo2 {
    public static void main(String[] args) throws IOException {
        //创建DatagramSocket对象,绑定主机的端口号
        DatagramSocket socket = new DatagramSocket(4000);
        byte[] buffer = new byte[1024];
        //创建空的数据报用于接收数据
        DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
        //接收数据报
        socket.receive(packet);
        String recev = new String(packet.getData(),0,packet.getLength());
        System.out.println(recev);
        String str = "welcome to Java World";
        //根据接收到的数据报中含有的IP地址和端口号信息创建要发送的数据报对象
        DatagramPacket packet2 = new DatagramPacket(str.getBytes(),str.length(),packet.getAddress(),packet.getPort());
        //发送数据报
        socket.send(packet2);
        socket.close();
    }
}

上述Demo中的两个main方法,一方只能是先收数据,再根据收到数据的IP和Port发送数据,而另一方只能是先发数据,然后再收数据。就像之前TCP的多线程Demo一样,双方之间的收发数据应该是任意的,随时的,而不是需要一方去等待。
UDP是无状态的,之前的做的TCP接到客户端请求后马上做一个线程,将连接对象Socket传递进去进行处理!但是UDP的话是没有连接对象的,只要消息包的概念!理想状况是根据客户端的地址来创建发送消息的线程,以下Demo中模拟的时候收到的消息包就直接做一个线程进行处理了。
服务端:

/**
 * 模拟的时候收到的消息包就直接做一个线程进行处理了,理想的话应该是根据客户端的地址来创建线程
 */
public class ServerMain {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(4000);
        while(true){
            byte[] data=new byte[1024];
            DatagramPacket packet=new DatagramPacket(data,data.length);
            socket.receive(packet);
            System.out.println(new String(packet.getData(),0,packet.getLength()));
            new ServerSendThread(socket,packet).start();
        }
    }
}
/**
 * 服务端发送数据报线程
 */
public class ServerSendThread extends Thread {
    private DatagramPacket packet;
    private DatagramSocket socket;
    public ServerSendThread(DatagramSocket socket,DatagramPacket packet){
        this.socket = socket;
        this.packet = packet;
    }
    public void run(){
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        while(true){
            try {
                String in = br.readLine();
                DatagramPacket sendPacket = new DatagramPacket(in.getBytes(),in.length(),packet.getAddress(),packet.getPort());
                socket.send(sendPacket);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端:

public class ClientMain {
    public static void main(String[] args) throws SocketException, UnknownHostException {
        DatagramSocket socket = new DatagramSocket();
        new ClientSendThread(socket).start();
        new ClientReceiveThread(socket).start();
    }
}
/**
 * 客户端发送数据报线程
 */
public class ClientSendThread extends Thread {
    private DatagramSocket socket;
    public ClientSendThread(DatagramSocket socket){
        this.socket = socket;
    }
    public void run(){
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        while(true){
            try {
                String in = br.readLine();
                //设置数据报发往的IP和端口号
                DatagramPacket packet = new DatagramPacket(in.getBytes(),in.length(), InetAddress.getLocalHost(),4000);
                socket.send(packet);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 客户端接收数据报线程
 */
public class ClientReceiveThread extends Thread {
    private DatagramSocket socket;
    public ClientReceiveThread(DatagramSocket socket){
        this.socket = socket;
    }
    public void run(){
        byte[] buffer;
        while(true){
            buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer,buffer.length);
            try {
                socket.receive(packet);
                String str = new String(packet.getData(),0,packet.getLength());
                System.out.println(str);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

版权声明:本文为MDreamlove原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。