程序员写网络程序,主要编写的是应用层代码。真正要发这个数据,需要上层协议调用下层协议,应用层要调用传输层,传输层给应用层提供一组 API ,统称为 socket API 。
简单的说,网络编程套接字就是操作系统给应用程序提供的一组API(叫做socket API)。
系统给提供的 socket API 主要有两组
1.基于 UDP 的 API
2.基于 TCP 的 API
那TCP和UDP协议有什么特点呢?
TCP:
1.有连接
使用 TCP 通信的双方,则需要刻意保存对方的相关信息。
2.可靠传输
3.面向字节流
4.全双工
UDP:
1.无连接
使用 UDP 通信的双方,不需要可以保存对端的相关信息
2.不可靠传输
3.面向数据报
4.全双工
1.有连接和无连接
是否需要单独记录下对方的信息,如果需要就是有连接,如果不需要,就是无连接。
有连接:可以理解成,通信双方各自记录了对方信息。 比如打电话就是有连接通信,需要先把连接接受了,才能通信。
无连接:比如我们发短信,直接投递,不需要接受连接,就能通信。当我们需要验证码的时候,临时填一下号码,服务器不需要刻意记录。
2.可靠传输与不可靠传输
可靠:发送方知道接收方是否成功发送数据。
不可靠:消息发了之后,不关注结果。
3.面向字流/数据报
面向字节流:以字节为单位进行传输,读写方式非常灵活。
面向数据报:以一个 UDP 数据报为基本单位进行传输,一个数据报会明确大小,一次发送/接收一个完整的数据报,不能是半个数据报。
4.全双工/半双工
全双工:一条路径,双向通信。
半双工:一条路径,单向通信。
那么这里UDP比TCP要简单一点我们先来学习UDP。
一、UDP socket
那么UDP socket中主要涉及到两个类:DatagramSocket 和 DatagramPacket。Datagram是数据报的意思。
1.客户端服务器程序—回显服务
这里回显的意思就是客户端发了个请求,服务器返回一个一模一样的响应。请求是啥,响应就是啥。
这里我们先建立一个network包,在这个包下建立两个类分别是服务器UdpEchoServer和客户端UdpEchoClient。
我们先写 UdpEchoServer 的代码:
1.进行网络编程的大前提第一步需要先准备好socket实例。
private DatagramSocket socket = null;
2.此处在构造服务器这边的socket对象的时候,就需要显式的绑定一个端口号port。
前面已经介绍端口号,端口号的作用是用来区分和管理不同端口的 。
抛出异常的原因:
public UdpEchoServer(int port) throws SocketException {
//构造 socket 的同时,指定要关联/绑定的端口
socket = new DatagramSocket(port);
}
3.启动服务器,这里我们需要知道服务器是被动接收请求的一方,主动发送请求的是客户端,DatagramPacket 刚才说过是表示一个 UDP 数据报,发送一次数据就是发送 DatagramPacket ,接收一次数据也就是在收一个 DatagramPacket 。
那么这里启动服务器分为三步:
step1:读取客户端发来的请求并解析。
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//每次循环 做三件事情
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//为了接收数据需要先准备好一个空的DatagramPacket对象,由recieve进行填充数据
//为了方便处理请求 把数据包转成String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
step2:根据请求计算响应。
//2.根据请求计算响应
String response = process(request);
step3:把响应写回到客户端。
//3.把响应结果写回到客户端
// 根据 response 字符串,构造一个 DataProgramPacket
//和请求 packet 不同,此处构造响应的时候,需要制定这个包要发给谁
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());
//requestPacket 是从客户端收来的 getSocketAddress 得到客户端的ip和端口
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
}
}
//根据请求计算响应
//由于写的这个是 回显 程序,请求是啥,响应就是啥
private String process(String request) {
return request;
}
这里requestPacket.getLength()这个长度不一定是4096,可能此处的UDP数据报最长是4096,实际的数据可能不够4096。
注意这里send方法的参数,也是DatagramPacket,需要把响应数据先构造成一个DatagramPacket再进行发送。
response.getByte()这里的参数也不再是一个空的数组,response是刚才根据请求计算得到的响应。DatagramPacket里面的数据就是String response的数据。
requestPacket.getSocketAddress();这个参数的作用就是表示要把数据发给哪个地址+端口。
SocketAddress可以视为一个类,里面包含了IP和端口。
UdpEchoServer完整代码:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
//需要先定义一个 socket 对象
//通过网络通信,必须要使用 socket 对象
private DatagramSocket socket = null;
//绑定一个端口,不一定能成功!
//如果某个端口已经被别的进程占用了,此时这里的绑定操作就会出错
//同一个主机上,一个端口,同一时刻,只能被一个进程绑定
public UdpEchoServer(int port) throws SocketException {
//构造 socket 的同时,指定要关联/绑定的端口
socket = new DatagramSocket(port);
}
//启动服务器的主逻辑
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
//每次循环 做三件事情
//1.读取请求并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//为了方便处理请求 把数据包转成String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应(此时省略这个步骤)
String response = process(request);
//3.把响应结果写回到客户端
// 根据 response 字符串,构造一个 DataProgramPacket
//和请求 packet 不同,此处构造响应的时候,需要制定这个包要发给谁
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
//requestPacket 是从客户端收来的 getSocketAddress 得到客户端的ip和端口
requestPacket.getSocketAddress());
socket.send(responsePacket);
System.out.printf("[%s:%d] req: %s, resp: %s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//这个方法是根据请求计算响应
//由于咱们写的这个是 回显 程序,请求是啥,响应就是啥
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
接着写客户端UdpEchoClient的代码。
我们可以发现在客户端这里就不用手动指定端口号了,使用无参版本的构造方法,即让操作系统自己分配一个空闲的端口号。
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//客户端启动,需要知道服务器在哪里
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
//对于客户端来说,不需要显示关联端口
//不代表没有端口,而是系统自动分配了个空闲的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
但是对于服务器来说,必须手动指定端口号,因为后序客户端需要根据这个端口号来访问到服务器(客户端是主动发起请求的一方,需要事先知道服务器的地址和端口)。
OK,那么客户端的代码书写的步骤是什么呢?
step1、先从控制台读取用户输入的字符串
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while(true){
//1.先从控制台读取一个字符串过来
//先打印一个提示符,提示用户要输入内容
System.out.print("->");
String request = scanner.next();
step2:把这个用户输入的内容,构造成一个UDP请求,并发送
构造的请求包含两部分信息:
1)数据的内容,request字符串。
//2.把字符串构造成 UDP packet,并进行发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
注意这里又使用到了一种DatagramPacket构造方法,既能构造数据,又能构造目标地址,这个目标地址是IP和端口分开的写法。
step3:从服务器读取响应数据并解析
//3.客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
step4:把响应结果转化为 String 显示到控制台上
//4.把响应数据转换成 String 显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req:%s,resp:%s\n",request,response);
UdpEchoClient完整代码:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIP;
private int serverPort;
//客户端启动,需要知道服务器在哪里
public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
//对于客户端来说,不需要显示关联端口
//不代表没有端口,而是系统自动分配了个空闲的端口
socket = new DatagramSocket();
this.serverIP = serverIP;
this.serverPort = serverPort;
}
public void start() throws IOException {
//通过这个客户端可以多次和服务器进行交互
Scanner scanner = new Scanner(System.in);
while(true){
//1.先从控制台读取一个字符串过来
//先打印一个提示符,提示用户要输入内容
System.out.print("->");
String request = scanner.next();
//2.把字符串构造成 UDP packet,并进行发送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);
socket.send(requestPacket);
//3.客户端尝试读取服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
//4.把响应数据转换成 String 显示出来
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.printf("req:%s,resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
udpEchoClient.start();
}
}
注意我们刚才已经写好了客户端的代码,那么在我们写客户端代码的过程中,已经早早的启动服务器了,就是说在写客户端代码的过程中,是没人访问服务器的,这里的服务器就在receive这里,阻塞等待了。
OK那么我们现在启动客户端,输入一个hello。
再点到我们的服务器这边,可以看到已经接收到客户端的请求,这个 64879 就是系统自动给客户端分配的端口。
好啦!今天的知识点涉及较多,下期继续讲解~