tcp和UDP都是关于网络的传输层中的相关协议。
下面我们就具体的来看一下这两个具体的差别
TCP | UDP | 解释 |
---|---|---|
有连接 | 无连接 | 有连接是先接通,然后才可以传输 eg:打电话 无连接是在不接通的情况下就可以传输 eg:微信消息 |
可靠传输 | 不可靠传输 | 可靠传输是可以知道对方有没有接收到消息 eg:钉钉的已读 不可靠传输时**不知道对方有没有收到消息 **eg:QQ,微信 |
面向字节流 | 面向数据报 | 面向字节流:传输的时候可以传输任意大小的数据 面向数据报:只可以传输完整的报文 |
全双工 | 全双工 | 全双工:一条链路,双向通信 半双工:一条链路,单向通信 |
UDP的socket比TCP简单
UDP的Socket编程主要涉及到一下两个类:
DatagramSocket:一个用作工具的类
DatagramPacket: 一个具体的数据报类,UDP传输的最小单位
这个类主要是操作类,负责关于网络编程的一系列的操作动作
DatagramSocket就是数据报
一个这样的DataSocket对象,就对应着一个socket文件。
这个socket文件不是别的,其实就是网卡(操作系统中将网卡视为文件)
从socket文件中读取数据,就是读网卡
从socket文件中写数据,就是写网卡
这里客户端和服务器的socket都使用的是DatagramSocket
如果指定了端口号,表示自己的设备的端口号
如果没有端口号,表示系统会自动为本设备匹配端口号
这个类的对象就是代表着一个具体的数据报,是UDP传输的基本单位。
我们最后传输的就一定是数据报也就是DatagramPacket的方式。
这个就是写一个回显服务,就是传入什么信息,就传出什么信息。
基本上不涉及什么具体的业务逻辑,只是使用socket进行转发而已
import java.net.DatagramSocket;
import java.net.SocketException;
public class EchoServer {
//首先这个服务器类必须有一个Socket对象
private DatagramSocket datagramSocket=null;//先设为null,用到的时候再赋值
public EchoServer(int port) throws SocketException {
//为socket初始化,注意这个socket要知道它的端口号才行
datagramSocket=new DatagramSocket(port);
}
}
- 对于我们这个服务器来说,必须需要一个DatagramSocket来帮助我们进行操作Scoket网卡
- 这个**DatagramSocket是必须需要一个端口号的,用于定位,**相当于一个电话号码,通过电话号码(端口号)来定位
- 这个端口号可以自己指定,上面的这个就是自己指定的,也可以让系统自动分配
- 因为自己指定的端口号可能会出现和原有的应用程序的端口号相同的情况,所以声明一个异常SocketException,另外一个进程打开多个文件也会出现这样的异常,如果已经打开了很多的文件,那么这个socket文件就有可能打不开
服务器的工作的主要内容
- 接收客户端的请求
- 根据请求做出相应
- 将相应返回给客户端
下面是具体的操作:
public void start() throws IOException {
System.out.println("start the server");
//因为UDP不需要进行全连接,所以直接开始工作:
while(true){
//1. 接收客户端的请求
//a. 先创建一个空盘子,之后好往里面输入数据
DatagramPacket receivePacket=new DatagramPacket(new byte[100],100);
//b. 将接收到的数据保存到数据报中
datagramSocket.receive(receivePacket);
//c. 将datagramPacket中的内容转化成一个字符串,一般都是转为字符串来进行交流
//注意:这里的datagramPacket就是将byte[]数组进行了简单封装,所以这里的getData就是原来的数组,getlength是接收到的数组的长度
String request=new String(receivePacket.getData(),0,receivePacket.getLength(),"UTF-8");
//2. 根据请求做出相应
//因为这是一个回显服务器,所以直接返回相同的内容
String respond=process(request);
//3. 将相应返回给客户端
//a. 先构造出一个packet,因为里面使用String,到了外层还是使用packet的
DatagramPacket respondPacket=new DatagramPacket(respond.getBytes(),respond.getBytes().length,
receivePacket.getSocketAddress());//必须要回传地址且传回的地址必须和传入的地址一样! ip和端口号
//b. 返回Packet
datagramSocket.send(respondPacket);
}
}
主要的流程就是:
receive(Packet receive)---------->Packet receive-------->String receive 传入
String receive--------->process()--------->String respond 处理
String respond-------Packet respond---------->send(Packet respond) 传出
主要的细节都在代码的注释里面
还有一个特别容易弄混的一点就是:
- 从Packet到String
String request=new String(receivePacket.getData(),0,receivePacket.getLength(),"UTF-8");
getData–直接获取数组
0 起始的坐标
getLength 获取的长度
utf-8 编码方式
- 从String到Packet
DatagramPacket respondPacket=new DatagramPacket(respond.getBytes(),
respond.getBytes().length,
receivePacket.getSocketAddress());
//必须要回传地址且传回的地址必须和传入的地址一样! ip和端口号
getByte 将字符转为byte[]数组
getByte().length 等到字节数组的长度
getSocketAddress() 等到ip地址和port端口号
import java.net.DatagramSocket;
import java.net.SocketException;
public class EchoClient {
private DatagramSocket datagramSocket=null;
//客户端需要指明服务器的地址,这样才知道向谁发送请求
private String ServerIp;
private int ServerPort;
public EchoClient(String ip,int port) throws SocketException {
//将Socket初始化,
//这个客户端的socket对象是不用手动指定端口号的,而是让系统自动分配
datagramSocket=new DatagramSocket();
this.ServerIp=ip;
this.ServerPort=port;
}
}
这里我们需要和服务器的构造函数进行一下是否有无端口号的对比:
首先我们再来复习一下子什么是端口号.
端口号是对应我们计算机中的应用程序来说的,每一个程序/进程都会被系统分配一个端口号,这个端口号都是对应唯一一个程序的,一个进程不可以匹配同一个端口号.
上面的服务器有手动分配端口号,但是这个客户端不是手动分配端口号,这是为什么呢?
因为服务器需要让所有的客户端都知道它的位置,所以它需要人为的确定一个我们知道的端口号,好让客户端发送请求的时候知道位置.
但是,对于客户端来说,客户端是客户电脑上面的程序,客户的电脑上面可能会有多个程序,这样的情况下,我们再手动的进行端口号的输入的话,很可能就会和别的程序的端口号冲突,所以我们对于客户端来说就不需要自己手动的输入端口号了
接着我们就可以开始正式的业务请求了,先是输入字符串,然后将字符串包装成数据报的形式,让socket转发给服务器
//1.从控制台中读取一个字符串
String s=scanner.nextLine();
//2.将字符串转化为一个数字报的形式
DatagramPacket requestPocket=new DatagramPacket(
s.getBytes(),
s.getBytes().length,
InetAddress.getByName("127.0.0.1"),9090);
//上面的数据报的构造函数是一共4个参数的
datagramSocket.send(requestPocket);
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class EchoClient {
private DatagramSocket datagramSocket=null;
//客户端需要指明服务器的地址,这样才知道向谁发送请求
private String ServerIp;
private int ServerPort;
public EchoClient(String ip,int port) throws SocketException {
//将Socket初始化,
//这个客户端的socket对象是不用手动指定端口号的,而是让系统自动分配
datagramSocket=new DatagramSocket();
//这里的是服务器的地址和端口
this.ServerIp=ip;
this.ServerPort=port;
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while(true){
//1.从控制台中读取一个字符串
String s=scanner.nextLine();
//2.将字符串转化为一个数字报的形式
DatagramPacket requestPocket=new DatagramPacket(s.getBytes(),s.getBytes().length,
InetAddress.getByName("127.0.0.1"),9090);
datagramSocket.send(requestPocket);
//3.接收到response数据报
//先准备好一个数据报好用来接收
DatagramPacket receivePocket=new DatagramPacket(new byte[1024],1024);
datagramSocket.receive(receivePocket);
//4.将数据报解析为字符串
String response=new String(receivePocket.getData(),
0,receivePocket.getLength(),
"UTF-8");
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
EchoClient echoClient=new EchoClient("127.0.0.1",9090);
echoClient.start();
}
}
服务器 | 客户端 | |
---|---|---|
源IP | 服务器自身ip | 客户端的自身ip |
源端口号 | 服务器自身的端口号 | 客户端的自身的端口号 |
目标ip | 收到的数据报的ip | 服务器的ip |
目标端口号 | 收到的数据报的端口号 | 服务器的端口号 |
协议类型 | UDP | UDP |
TCp的socket的api主要是包括,
serverSocket负责接收来自于客户端传来的消息
socket是我们传输字节流的的基本单位,如服务器接收到的就是socket对象
客户端发给服务器的也是socket对象
//TCP也是需要有一个Socket来帮助我们来进行操控
private ServerSocket serverSocket=null;
//这个端口号是我们为当前的服务器指定的
public TCPEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
因为TCP协议是有连接的,所以我们这里就不能像UDP一样直接就工作了,我们要先是等待客户端连上了以后,我们才可以进行以后的操作
就是类比一下:
服务器就是像客户端进行打电话的一位,
如果客户端一直不接听,它们之间就不会连接,不会有之后的工作
如果客户端接听了,它们才算是连接了,可以进行工作了
注意:
为了实现一个服务器对应多个客户端我们就可以使用多线程的方式:
在while循环中,只要是接收到了客户端的消息,我们就创建一个线程来对这个消息进行单独的处理,这样就可以达到一对多的效果了
public void start() throws IOException {
while(true){
System.out.println("TCP服务器启动");
//1.TCP协议需要先建立连接才可以,
// 所以这里的socket要等到客户端和服务器接通才可以正常工作
//2.返回一个Socket对象----clientSocket,将接收到的客户端的请求,
// 以后的客户端和服务器之间的工作都通过clientSocket来进行
Socket clientSocket=serverSocket.accept();
//[注意]为了实现一个服务器对应多个客户端,我们使用多线程的方式.
//对每一个新接收到的socket都创建一个新的线程来进行处理
Thread thread=new Thread(()->{
try {
connection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
}
比上面的多线程更加高效的是,我们还可以使用线程池来更好的进行处理
创建一个线程池
在线程池中提交一个又一个的线程
public void start() throws IOException {
ExecutorService pool= Executors.newCachedThreadPool();
while(true){
System.out.println("TCP服务器启动");
//1.TCP协议需要先建立连接才可以,
// 所以这里的socket要等到客户端和服务器接通才可以正常工作
//2.返回一个Socket对象----clientSocket,将接收到的客户端的请求,
// 以后的客户端和服务器之间的工作都通过clientSocket来进行
Socket clientSocket=serverSocket.accept();
//[注意]为了实现一个服务器对应多个客户端,我们使用线程池的方式.
//对每一个新接收到的socket都放到线程池中进行处理
pool.submit(new Runnable() {
@Override
public void run() {
try {
connection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
这里TCP的传输方式是面向字节流和UDP的数据报的方式不一样,直接像文件的读写一样进行消息的接收和回传就可以
过程:
- 打开socket的输入流和输出流
- 用Scanner来读取输入流
- 进行possess处理
- 使用PrintWriter来写入输出流
- 如果没有请求了,就断开和服务器的连接
private void connection(Socket clientSocket) throws IOException {
System.out.printf("服务器和客户端[%s %d]建立了连接\n",
clientSocket.getInetAddress().toString(),clientSocket.getPort());
//这里和文件的读和写一样,只不过这个输入流和输出流都是socket中的客户端的输入和服务器的输出
try(InputStream inputStream=clientSocket.getInputStream()) {
try(OutputStream outputStream=clientSocket.getOutputStream()){
while(true){
//1.读取客户端的请求
//使用Scanner来代替inputStream进行读取 方便
Scanner scanner=new Scanner(inputStream);
//如果没有读取内容了,就没有请求了,就可以断开连接了
if(!scanner.hasNext()){
System.out.println("服务器和客户端断开连接");
break;
}
//读取到的字符串请求
String request=scanner.next();
//2.做出响应
String response=possess(request);
//3.写回客户端
// 将outputStream包装成PrintWriter,方便
PrintWriter printWriter=new PrintWriter(outputStream);
// 写操作,不要看它是println,不是打印操作,而是写操作
printWriter.println(response);
// 及时的刷新到客户端中,使客户端可以及时的收到
printWriter.flush();
System.out.printf("服务器对客户端[%s %d]的request:%s的response是%s\n",
clientSocket.getInetAddress().toString(),
clientSocket.getPort(),
request,response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
clientSocket.close();
}
}
private String possess(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer=new TCPEchoServer(9090);
tcpEchoServer.start();
}
客户端的构造函数就只有有Socket对象就可以了.
另外,这个socket的构造函数的端口号和ip指的是服务器的端口号和IP地址
//这里的客户端使用的是这样的socket
Socket socket=null;
public TCPEchoClient(String ip,int port) throws IOException {
//这里的ip和port是指服务器的ip和port,为了找到服务器的位置
socket=new Socket(ip,port);
}
这个客户端的工作流程和服务器的很像:
- 先打开socket的inputStream和outputStream
- 先从键盘中读取请求
- 将请求用printWriter写到outputStream中,
- 用scanner读取inputStream中的响应
public void start(){
System.out.println("和服务器建立连接");
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream()){
try(OutputStream outputStream=socket.getOutputStream()){
while(true){
//1.从键盘读取请求
System.out.println("->");
String request=scanner.next();
//2.将请求发送给服务器
// 为了方便,还是使用printWriter对outputStream包装
PrintWriter printWriter=new PrintWriter(outputStream);
// 写操作
printWriter.println(request);
// 刷新缓冲区,防止服务器接收不到
printWriter.flush();
//3.从服务器接收请求
Scanner responseScanner=new Scanner(inputStream);
String response=responseScanner.next();
//4.打印接收到的请求
System.out.printf("req:%s resp:%s\n",request,response);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient=new TCPEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}