打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Java网络通信 TCP、UDP
userphoto

2022.07.14 广东

关注
网络程序设计基础
前言:
这边文章是一篇读书笔记,是我个人在看《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 实例,并监听给定端口号 servPort14 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 // 关闭 Socket35 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 // 关闭 Socket40 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程序的步骤:
发送数据包:
使用DatagramSocket()创建一个数据包套接字。
使用DatagramPacket(byte[] buf, int length, InetAddress address, int port)创建要发送的数据包。
使用DatagramSocket类的send()方法发送数据包。
接收数据包:
使用DatagramSocket(int port)创建数据包套接字,绑定到指定的端口。
使用DatagramPacket(byte[] buf, int length)创建字节数组来接收数据包。
使用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 // 创建数据报文 Socket11 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" 1024011 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 // 获取服务端端口号,默认 1024121 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 // 关闭 Socket62 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 报文能承载的最大数据。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
TCP与UDP初探
【从零开始学Java笔记】网络编程
终于有人把TCP协议与UDP协议给搞明白了
搞了两周Socket通信,终于弄明白了!--文末送书
第二十六天 网络编程【悟空教程】
「面试」计算机网络基础知识点
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服