Java网络通信 TCP、UDP

网络程序设计基础

前言:

  这边文章是一篇读书笔记,是我个人在看《Java从入门到精通》(第四版)一书时整理的一个笔记。里面也有借鉴到https://blog.csdn.net/wyzidu/article/details/83826656中的相关内容。如果内容涉及侵权,望告知。后面会及时删除。

1.1 局域网与因特网

为了实现两台计算机的通信,必须用一个网络线路连接两台计算机,如下图所示:

1.2 网络协议

啥是网络协议?

  网络协议就是规定了计算机之间连接的物理,机械(网线与网卡的连接规定)、电气(有效的电平范围)等特征以及计算机之间的相互寻址规则、数据发送冲突的解决、长的数据如何分段传送与接收等。就像不同国家有不同的法律一样,目前网络协议也有多种。

(1)IP协议

  IP是Internet Protocol的简称,它是一种网络协议。Internet网络采用的协议是TCP/IP协议,其全称是Transmission Control Protocol/Internet Protocol。Internet依靠TCP/IP协议,在全球范围内实现不同硬件结构、不同操作系统、不同的网络系统的互联。

TCP/IP模式是一种层次结构,共分为4层,分别为应用层、传输层、网络层和链路层。如下图所示:

 

 

1.3 端口和套接字

  一般而言,一台计算机只有单一的连到网络的物理连接,所有的数据都通过此连接对内、对外送达特定的计算机,这就是端口。而网络程序中设计的端口并非真实的物理存在,而是一个假想的连接设置。端口被规定为一个在0~65535之间的整数。HTTP服务一般使用80端口,FTP服务一般使用21端口。假如一台计算机提供了HTTP、FTP等多种服务,那么客户机会通过不同的端口来确定连接到服务器上的哪项服务上。如下图所示:

网络程序中的套接字(socket)用于将应用程序与端口连接起来。套接字是一个假想的连接装置,就像插座一样,用于连接电器和插口。Java将套接字抽象化为类,程序设计者只需要创建Socket类对象,即可使用套接字。如下图所示:

2. TCP程序设计基础

      TCP网络程序设计是指利用Socket类编写的通信程序。利用TCP协议进行通信的两个应用程序是有主次之分的,一个称为服务器程序,另一个称为客户机程序,两者的功能和编写方法大不一样。服务器端与客户机端的交互过程如下图所示:

1. 服务器程序创建一个ServerSocket(服务器端套接字),调用accrpt()方法等待客户机来连接;

2.客户端程序创建一个Socket,请求与服务器建立连接;

3.服务器接收客户机的连接请求,同时创建了一个新的Socket与客户机建立连接,服务器继续等待新的请求。

得到一张比较详细的Socket请求过程,如下图:

2.1 InetAddress类

 1 import java.net.InetAddress;
 2 import java.net.UnknownHostException;
 3 /**
 4  * 测试InetAddress类的常用方法
 5  */
 6 public class testInetAddressApi {
 7     public static void main(String[] a) {
 8         InetAddress ip;
 9         try {
10             ip = InetAddress.getLocalHost();// 实例化对象
11             String localname = ip.getHostName();// 获取本级名
12             String localip = ip.getHostAddress();// 获取本级ip地址
13             System.out.println("本机名:"+ localname);
14             System.out.println("本机IP地址:"+ localip);
15         } catch (UnknownHostException e) {
16             e.printStackTrace();
17         }
18     }
19 }

输出结果:

本机名:ppp-99-12-203-170.dsl.scrm01.sbcglobal.net
本机IP地址:99.12.203.170

2.2 ServerSocket类

      java.net包中的ServerSocket类用于表示服务器套接字,其主要功能是等待来自网络上的请求,它可通过指定的端口来等待连接的套接字。服务器套接字一次可以与一个套接字连接。如果多台客户机同时提出连接请求,服务器套接字会将请求连接的客户机存入队列中,然后从中取出一个套接字,与服务器新建的套接字连接起来。若请求连接数大于最大容纳数,则多出的连接请求被拒绝。队列的大小默认为50。

ServerSocket类的构造函数都抛出IOException异常分别有以下几种形式:

  • ServerSocket():创建非绑定服务器套接字。
  • ServerSocket(int port):创建绑定到特定端口的服务器套接字。
  • ServerSocket(int port, int backlog):利用指定的backlog创建服务器套接字并将其绑定到指定的本地端口号。  
  • ServerSocket(int port, int backlog, InetAddress bindAddress):使用指定的端口、侦听backlog和要绑定到本地IP地址创建服务器。这种情况适用于计算机上有多块网卡和多个IP地址的情况,用于可以明确规定ServerSocket在哪块网卡或IP地址上等待客户机的连接请求。

关于第三个构造函数ServerSocket(int port, int backlog),有点需要自己注意的地方:

对于第二个参数backlog,java文档是这样解释的:

The maximum queue length for incoming connection indications (a request to connect) is set to the backlog parameter. If a connection indication arrives when the queue is full, the connection is refused.

所以一开始我是认为这个参数表示着请求待处理队列的长度。所以我就想,如果要控制客户机的连接数,是不是把backlog这个参数设置成自己想要的值就可以控制了。然后我设置成1,用两个客户机去连接,但是没有报错。这个时候就有点懵逼。上网查了一下说要把连接数设置大一点,接着我就把客户机连接数扩大45,果然这次出现了异常,就抛ConnectException了。难道是连接数太少?可是这样并没办法说服自己。后来我想这个值表示的是队列的长度,其实一开始java文档并没有说这个事请求连接数的数量。那么有可能一开始我把backlog设置成1,用两个客户机去请求连接,服务器很快就把连个连接处理完了,所以队列长度为1并没有造成大小不够。按照这个思路,第二次实验的时候,我用单独的线程处理服务器处理请求,在其处理请求的逻辑里让其休眠5秒,然后再用两个客户机去请求。这个时候果然抛了ConnectException异常。以此得以验证我的想法是没有错误的。那么用45客户机请求,我把backlog设置成45或者更大,理应也是不会出现异常的。实验之后果然是正常的。由此可知,想控制请求的连接数并不能通过设置backlog的值来达到效果。

 ServerSocket类的常用方法:

方法返回值说明
accept()Socket 等待客户机连接。若连接,则创建一套接字
isBound()boolean判断ServerSocket的绑定状态
getInetAddress()InetAddress返回此服务器套接字的本地地址
isClosed()boolean返回服务器套接字的关闭状态
close()void关闭服务器套接字
bind(SocketAddress endpoint)void将ServerSocket绑定到特定的地址(IP地址和端口号)
getInetAddress()void返回服务器套接字等待的端口号

关于accept()方法做如下备注说明,方便理解:

1. 调用ServerSocket类的accpet()方法会返回一个和客户机端Socket对象相连接的Socket对象,服务器端的Socket对象使用getOutputStream()方法获得输出流将指向客户端Socket对象使用getInputStream()方法获得的那个输入流;同样,服务器端的Socket对象使用getInputStream()方法获得的输入流将指向客户机端Socket对象使用getOutputStream()方法获得的那个输入流。也就是说,当服务器向输出流写入信息时,客户端通过相应的输入流就能读取,反之亦然。

2. accpet()方法会阻塞线程继续执行,直到接收到客户的呼叫。如果没有客户呼叫服务器,那么如下的语句中,System.out.println("连接中。。。")将不会被执行。语句如果没有客户请求,而accpet()方法没有发生阻塞,那么肯定是程序出现了问题。通常是使用了一个还在被其他程序占用的端口号,ServerSocket绑定不成功。

1 Socket s = server.accpet();
2 System.out.println("连接中。。。");

2.3 TCP网络程序

  明白了TCP程序的工作的过程,就可以开始编写TCP服务程序了。在网络编程中如果只要客户机向服务器发送消息,不要求服务器向客户机发送消息,称为单向通信。如果服务器也向客户机返回消息,则是双向通信。下面是一个简单的双向通信案例:

服务器端:

 1 import java.io.IOException;
 2 import java.io.InputStream;
 3 import java.io.OutputStream;
 4 import java.net.ServerSocket;
 5 import java.net.Socket;
 6 import java.net.SocketAddress;
 7 
 8 public class TcpEchoServer {
 9     private static final int BUFSIZE = 32;
10 
11     public static void main(String[] args) throws IOException {
12 
13         // 创建 ServerSocket 实例,并监听给定端口号 servPort
14         ServerSocket servSock = new ServerSocket(6660);
15         int recvMsgSize;
16         byte[] receiveBuf = new byte[BUFSIZE];
17 
18         while (true) {
19             // 用于获取下一个客户端连接,根据连接创建 Socket 实例
20             Socket clntSock = servSock.accept();
21             // 获取客户端地址和端口号
22             SocketAddress clientAddress = clntSock.getRemoteSocketAddress();
23             System.out.println("Handling client at " + clientAddress);
24 
25             // 获取 socket 的输入输出流
26             InputStream in = clntSock.getInputStream();
27             OutputStream out = clntSock.getOutputStream();
28 
29             // 每次从输入流中读取数据并写到输出流中,直至输入流为空
30             while ((recvMsgSize = in.read(receiveBuf)) != -1) {
31                 out.write(receiveBuf, 0, recvMsgSize);
32             }
33 
34             // 关闭 Socket
35             clntSock.close();
36         }
37     }
38 }

客户端:

 1 import java.io.IOException;
 2 import java.io.InputStream;
 3 import java.io.OutputStream;
 4 import java.net.Socket;
 5 import java.net.SocketException;
 6 
 7 public class TcpEchoClient {
 8     public static void main(String[] args) throws IOException {
 9 
10         // 根据参数创建 Socket 实例
11         Socket socket = new Socket("127.0.0.1", 6660);
12 
13         System.out.println("Connected to server... sending echo string");
14 
15         // 获取 socket 的输入输出流
16         InputStream in = socket.getInputStream();
17         OutputStream out = socket.getOutputStream();
18 
19         // 要发送的信息
20         String sendMsg = "这是测试请求服务端的程序。。。";
21 
22         // 将数据写入到 Socket 的输出流中,并发送数据
23         byte[] data = sendMsg.getBytes();
24         out.write(data);
25 
26         int totalBytesRcvd = 0;
27         int bytesRcvd;
28 
29         // 接收返回信息
30         while (totalBytesRcvd < data.length) {
31             if ((bytesRcvd = in.read(data, totalBytesRcvd, data.length - totalBytesRcvd)) == -1) {
32                 throw new SocketException("Connection closed permaturely");
33             }
34             totalBytesRcvd += bytesRcvd;
35         }
36 
37         System.out.println("Received: " + new String(data));
38 
39         // 关闭 Socket
40         socket.close();
41     }
42 }

必要说明:

1. Socket 中的输入输出流是流抽象,可看做一个字符序列,输入流支持读取字节,输出流支持取出字节。每个 Socket 实例都维护了 一个 InputStream 和一个 OutputStream 实例,数据传输也主要依靠从流中获取数据并解析的过程。
2. ServerSocket 与 Socket 区别,ServerSocket 主要用于服务端,用于为新的 TCP 连接请求提供一个新的已连接的 Socket 实例。Socket 则用于服务端和客户端,用于表示 TCP 连接的一端。因此,服务端需要同时处理 ServerSocket 和 Socket 实例,而客户端只需要处理 Socket 实例即可。
3. 发送数据时只通过 write() 方法,接收时为何需要多个 read() 方法?
TCP 协议无法确定在 read() 和 write() 方法中所发送信息的界限,而且发送过程中可能存在乱序现象,即分割成多个部分,所以无法通过一次 read() 获取到全部数据信息。

3. UDP程序设计基础

  用户数据报协议(UDP)是网络信息传输的另一种形式。基于UDP的通信和基于TCP通信不同,基于UDP的信息传输更快,但是不提供可靠的保证。使用UDP传递数据时,用户无法知道数据能否正确地到达主机,也不能确定到达目的的顺序是否和发送的顺序相同。虽然UDP是一种不可靠的协议,但是如果需要较快地传输信息,并能容忍小的错误,可以考虑使用UDP.

基于UDP通信的基本模式如下:

  • 将数据打包(称为数据包),然后将数据包发往目的地。
  • 接收别人发来的数据包,然后查看数据包。

下面是总结UDP程序的步骤:

发送数据包:

  1. 使用DatagramSocket()创建一个数据包套接字。
  2. 使用DatagramPacket(byte[] buf, int length, InetAddress address, int port)创建要发送的数据包。
  3. 使用DatagramSocket类的send()方法发送数据包。

接收数据包:

  1. 使用DatagramSocket(int port)创建数据包套接字,绑定到指定的端口。
  2. 使用DatagramPacket(byte[] buf, int length)创建字节数组来接收数据包。
  3. 使用DatagramPacket类的receive()方法接收UDP包。

注意:

DatagramSocket类的receive()方法接收数据时,如果还没有可以接收到数据,在正常情况下receive()方法将阻塞,一直等到网络上有数据传过来,receive()方法接收数据并返回。如果网络上没有数据发送过来,receive()方法也没有阻塞,肯定是程序有问题,大多数是使用了一个被其他程序占用的端口号。

3.1 DatagramSocket类

DatagramSocket类用于表示发送和接收数据包的套接字。该类的构造函数有:

  • DatagramSocket()
  • DatagramSocket(int port)
  • DatagramSocket(int port, InetAddress addr)

  第一种构造函数创建DatagramSocket对象,构造数据报套接字并将其绑定到本地主机上任何可用的端口。第二种构造函数创建DatagramSocket对象,创建数据报套接字并将其绑定到本地主机上指定的端口。第三种构造函数创建DatagramSocket对象,建数据报套接字并将其绑定到指定的本地地址。第三种构造函数适用于多块网卡和多个IP地址的情况。

3.2 DatagramPacket类

DatagramPacket类用来表示数据包。Datagrampacket类的构造函数有:

  • DatagramPacket(byte[] buf, int length)
  • DatagramPacket(byte[] buf, int length, InetAddress address, int port)

  第一种构造函数创建DatagramPacket对象,指定了数据包的内存空间和大小。第二种构造函数不仅指定了数据包的内存空间和大小,还指定了数据包的目标地址和端口。在发送数据时,必须指定接收方的Socket地址和端口号,因此使用第二种构造函数可创建发送数据的DatagramPacket对象。

下面是UDP实例:

服务端:

 1 import java.io.IOException;
 2 import java.net.DatagramPacket;
 3 import java.net.DatagramSocket;
 4 import java.net.SocketException;
 5 
 6 public class UDPEchoServer {
 7     private static final int ECHOMAX = 255;
 8 
 9     public static void main(String[] args) throws IOException {
10         // 创建数据报文 Socket
11         DatagramSocket socket = new DatagramSocket(6661);
12         // 创建数据报文
13         DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
14 
15         while (true) {
16             // 接收请求报文
17             socket.receive(packet);
18             System.out.println("Handling client at " + packet.getAddress().getHostAddress() +
19                     " on port " + packet.getPort());
20 
21             // 发送数据报文
22             socket.send(packet);
23             // 重置缓存区大小
24             packet.setLength(ECHOMAX);
25         }
26     }
27 }

客户端:

运行参数配置:

 1 import java.io.IOException;
 2 import java.io.InterruptedIOException;
 3 import java.net.*;
 4 
 5 public class UDPEchoClient {
 6     private static final int TIMEOUT = 3000;
 7     private static final int MAXTRIES = 5;
 8 
 9     public static void main(String[] args) throws IOException {
10         // 参数解析,格式 url "info" 或 url "info" 10240
11         if ((args.length < 2) || (args.length > 3)) {
12             throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]");
13         }
14 
15         // 创建目标 Server IP 地址对象
16         InetAddress serverAddress = InetAddress.getByName(args[0]);
17 
18         // 将需传输字符转换为字节数组
19         byte[] byteToSend = args[1].getBytes();
20         // 获取服务端端口号,默认 10241
21         int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 6661;
22 
23         // 创建 UDP 套接字,选择本地可用的地址和可用端口号
24         DatagramSocket socket = new DatagramSocket();
25 
26         // 设置超时时间,用于控制 receive() 方法调用的实际最短阻塞时间
27         socket.setSoTimeout(TIMEOUT);
28 
29         // 创建发送数据报文
30         DatagramPacket sendPacket = new DatagramPacket(byteToSend, byteToSend.length, serverAddress, servPort);
31 
32         // 创建接收数据报文
33         DatagramPacket receivePacket = new DatagramPacket(new byte[byteToSend.length], byteToSend.length);
34 
35         // 设置最大重试次数,以减少数据丢失产生的影响
36         int tries = 0;
37         // 是否收到响应
38         boolean receivedResponse = false;
39         do {
40             // 将数据报文传输到指定服务器和端口
41             socket.send(sendPacket);
42             try {
43                 // 阻塞等待,直到收到一个数据报文或等待超时,超时会抛出异常
44                 socket.receive(receivePacket);
45                 // 校验服务端返回报文的地址和端口号
46                 if (!receivePacket.getAddress().equals(serverAddress)) {
47                     throw new IOException("Received packet from an unknown source");
48                 }
49                 receivedResponse = true;
50             } catch (InterruptedIOException e) {
51                 tries += 1;
52                 System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
53             }
54         } while (!receivedResponse && (tries < MAXTRIES));
55 
56         if (receivedResponse) {
57             System.out.println("Received: " + new String(receivePacket.getData()));
58         } else {
59             System.out.println("No response -- giving up.");
60         }
61         // 关闭 Socket
62         socket.close();
63     }
64 }

必要说明:

1. UDP服务端 与 TCP 服务端不同,TCP 对于每一个客户端请求都需要先建立连接,而 UDP 则不需要。因此,UDP 只需创建一个 Socket 等待客户端连接即可。

2. 在该 UDP 服务器的实现中,只接收和发送数据报文中的前 ECHOMAX 个字符,超出部分直接丢弃。

3. 在处理过接收到的消息后,数据包的内部长度会设置为刚处理过的消息长度,通常比初始长度要短,因此需重置缓冲区为初始长度。否则后续可能会使得缓冲区长度不断减小,使得数据包被截断。

4. 由于 UDP 提供的是尽最大可能的交付,所以在发送 Echo Request 请求时,无法保证一定可以送达目标地址和端口,因此考虑设置重传次数,若在超过最大等待时间后仍未收到回复,则重发当前请求,若重发次数超过最大重试次数,则可直接返回未发送成功。

4. UDP Socket 与 TCP Socket 区别

  • UDP 保存了消息的边界信息,而 TCP 则没有。在 TCP 中需通过多次 read() 来接收一次 write() 的信息,而 UDP 中对于单次 send() 的数据,最多只需一次 receive() 调用。
  • TCP 存在传输缓冲区,UDP 则无需对数据进行缓存。由于 TCP 存在错误重传机制,因此需保留数据的缓存,以便于重传操作,当调用 write() 方法并返回后,数据被复制到传输缓冲区中,数据有可能处于发送过程中或还没有发生传送。而 UDP 则不存在该机制,因此无需缓存数据,当调用 send() 方法返回后,消息处于发送过程中。
  • UDP 会丢掉超过最大长度限制的数据,而 TCP 不会
  • 在 TCP 中,一旦建立连接后,对于所有数据都可以看做一个连续的字节序列。而在 UDP 中接收到的消息则可能来自于不同的源地址和端口,因此会将接收到的数据放在消息队列中,按照顺序来响应,超过最大长度的消息直接截断。Datagrampacket 所能传输的最大数据量为 65507 字节,也就是一个 UDP 报文能承载的最大数据。
     

转载于:https://www.cnblogs.com/zhongjunbo555/p/11327583.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值