文章目录
一、网络编程
我们网络编程的核心: Socket API,操作系统为我们应用程序提供的API,我们的Socket是和传输层密切相关的。
我们传输层为我们提供了两个最核心的协议UDP/TCP,所以我们的Socket API也为我们提供了TCP/UDP。
简单认识一下TCP/UDP:
TCP 有连接 可靠传输 面向字节流 全双工
UDP 无连接 不可靠传输 面向数据报 全双工
TCP:
特点:
- 使用TCP协议,必须双方先建立连接,它是一种面向连接的可靠通信协议
- 传输前,采用”三次握手"方式建立连接,所以是可靠的
- 在连接中可进行大数据量的传输
- 连接、发送数据都需要确认,且传输完毕后,还需释放已建立的连接,通信效率低
应用场景:对信息安全要求较高的场景,例如:文件下载、金融等数据通信
TCP:
特点:
- UDP是一种无连接,不可靠传输协议
- 将源IP、目的IP和端口封装成数据包,不需要建立连接
- 每个数据包大小限制在64kb内
- 发送不管对方是否准备好,接收方收到也不确认,所以是不可靠的
- 可以广播发送,发送数据结束时无需释放资源,开销小,速度快 应用场景: 语音通话,视频会话等
二、UDP数据报套接字编程
DatagramSocket
DatagramSocket 这个类表示一个Socket对象,我们操作系统中,把socket对象是当作一个文件来处理的。
一个Socket对象就可以与另一台主机进行通信了,如果要和不同的主机通信,就需要创建多个Socket对象
方法作用DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到任意一个随机端口号(一般用于客户端)DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定端口(一般用于服务器)方法作用void receive(DatagramPacket p)从此套接字接收数据,如果没有接收到数据报,进行阻塞等待)void send(DatagramPacket p)从此套接字发送数据包(不会阻塞等待,直接发送)void close()关闭此数据报套接字
我们的receive方法参数传入的是一个空的对象,receive方法内部会对这个对象进行填充,从而构造出结果数据,我们称这样的参数为输出型参数
DatagramPacket
DatagramPacket是UDP socket进行发送和接收的数据报。
方法作用DatagramPacket(byte[] buf,int length)构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组里,接受指定长度DatagramPacket(byte[] buf,int offset,int length,SocketAddress address)构造一个DatagramPacket用来发送数据报,发送的数据为字节数据,从0到指定长度,address用来指定目的主机的IP和端口号
DatagramPacket的一些方法:
方法作用InetAddress getAddress()从接受的数据报中,获取发送端IP地址,或从发送的数据报中,获取接收端主机IP地址int getPort()从接收的数据报中,获取发送端主机的端口号,或从发送的数据报中,获取接收端的端口号byte[] getData()获取数据报的数据
实现客户端服务器程序
我们在这里编写一个最简单的客户端服务器程序:回显服务器(echo server).
我们的服务器做的工作:收到请求,根据请求计算响应,返回响应,最重要的环节就是计算响应这一部分,我们的echo server省略了这一部分,接收到什么就返回什么。
EchoServer
我们网络编程,本质上是要操作网卡,但是网卡不方便我们直接操作,于是我们操作系统内核中,使用“socket"这样的文件来抽象表示网卡,所以我们要想进行网络通信,首先得有一个socket对象
publicclassEchoServer{privateDatagramSocket socket =null;}
我们的服务器,在真正创建对象的时候,需要绑定一个具体的端口号,为啥是具体的呢?因为我们的服务器在进行网络通信中,是属于比较被动的一方,如果我们使用的是系统随机进行分配的端口号,那么我们的客户端就不知道服务器端口号是多少,也就无法进行通信了。
publicEchoServer(int port)throwsSocketException{
socket =newDatagramSocket(port);}
我们的UDP传输的基本单位是DatagramPacket.
此时我们服务器接收到的DatagramPacket是一个特殊的对象,并不方便我们直接进行处理,我们可以把这里包含的数据拿出来,构造成一个字符串。
String request =newString(requestPcket.getData(),0, requestPcket.getLength());
我们在创建DatagramPacket给的最大长度是4096,但我们实际可能只用了一小部分,因此我们在构造字符串时,通过getLength()获取数据报实际的长度。
我们获取到了客户端的请求之后然后我们对请求进行处理,我们这里实现的是接收什么,回应什么。
publicStringprocess(String request){return request;}
然后我们将这个响应发送给客户端,首先我们需要构造出DatagramPacket对象。
DatagramPacket responsePacket =newDatagramPacket(response.getBytes(),response.getBytes().length);
因为我们的DatagramPacket不认字符只认字节,所以我们将response转换为字节数组。
答案是不可以,因为response.length()获取的是字符的个数,response.getBytes().length获取的是字节的个数。
我们这里的DatagramPacket的构造还是有一点点问题,我们这里的数据是构建好了,那给谁发呢?
DatagramPacket responsePacket =newDatagramPacket(response.getBytes(),response.getBytes().length,
requestPcket.getSocketAddress());
我们在参数中,应该传入客户端的地址信息。
然后进行发送
socket.send(responsePacket);
数据到达网卡,经过内核层层分用,最终到达了UDP传输层协议,调用receive相当于是执行内核中udp相关的代码,将UDP数据报的载荷取出来,拷贝到用户提供的byte[] 数组中。
publicclassEchoServer{privateDatagramSocket socket =null;publicEchoServer(int port)throwsSocketException{
socket =newDatagramSocket(port);}publicvoidstart()throwsIOException{System.out.println("服务器启动!");while(true){DatagramPacket requestPcket =newDatagramPacket(newbyte[4096],4096);
socket.receive(requestPcket);String request =newString(requestPcket.getData(),0, requestPcket.getLength());String response =process(request);DatagramPacket responsePacket =newDatagramPacket(response.getBytes(),response.getBytes().length,
requestPcket.getSocketAddress());
socket.send(responsePacket);//打印本次请求响应的结果System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPcket.getAddress().toString(),
requestPcket.getPort(),request,response);}}publicStringprocess(String request){return request;}}
客户端
我们在构造客户端Socket对象时,不需要显式的去绑定一个端口,而是由系统分配一个空闲端口。
服务器的端口:需要固定指定,为了方便客户端找到服务器程序。
客户端的端口:由系统自动分配的,如果我们手动指定,可能会与客户端其他程序的端口冲突
为什么服务器不怕冲突?
因为服务器上面的程序可控,而客户端是运行在我们用户电脑上,环境复杂,更不可控。
首先我们需要创建一个Socket对象,并且获取服务器的ip和端口号
privateDatagramSocket socket =null;privateString serverIp =null;privateint serverPort =0;publicEchoClient(String serverIp,int serverPort)throwsSocketException{
socket =newDatagramSocket();this.serverIp = serverIp;this.serverPort = serverPort;}
我们一次网络通信涉及到五元组:
源IP,源端口,目的IP,目的端口,协议类型。
然后从控制台接收我们需要发送端数据。
System.out.print("> ");String request = scan.next();if(request.equals("exit")){System.out.println("客户端退出");break;}
我们将request字符串构造成DatagramPacket进行发送。
DatagramPacket requestPacket =newDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
我们在构造DatagramPacket的时候,需要将ip和端口号都传入,此处需要传入的IP是32位的整数形式,但我们这里的ip是字符串,所以需要使用InetAddress.getByName进行转换,然后进行发送。
socket.send(requestPacket);
我们将请求发送给服务器之后,我们来接收一下服务器的响应。
DatagramPacket responsePacket =newDatagramPacket(newbyte[4096],4096);
socket.receive(responsePacket);String response =newString(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);
publicclassEchoClient{privateDatagramSocket socket =null;privateString serverIp =null;privateint serverPort =0;publicEchoClient(String serverIp,int serverPort)throwsSocketException{
socket =newDatagramSocket();this.serverIp = serverIp;this.serverPort = serverPort;}publicvoidstart()throwsIOException{System.out.println("客户端启动!");Scanner scan =newScanner(System.in);while(true){System.out.print("> ");String request = scan.next();if(request.equals("exit")){System.out.println("客户端退出");break;}DatagramPacket requestPacket =newDatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);DatagramPacket responsePacket =newDatagramPacket(newbyte[4096],4096);
socket.receive(responsePacket);String response =newString(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}}}
我们分别启动客户端和服务器
我们此处显示的客户端IP是环回IP,端口号是系统随机分配的。
我们客户端服务器程序,一个服务器是给许多客户端提供服务的,但是我们IDEA默认只能启动一个客户端,我们需要手动设置一下。
现在我们就可以创建多客户端与服务器进行通信了。
端口冲突
一个端口只能被一个进程使用,如果有多个使用就不行。
版权归原作者 熬夜磕代码丶 所有, 如有侵权,请联系我们删除。