运输层TCP/UDP协议
一、前言
我们知道负责运输层就是负责两个主机中进程之间的通信提供服务,由于一个主机可以运行多个进程,因此运输层有复用和分用的功能。复用就是多个应用进程可以同时使用下面的运输层的服务,分用则是运输层把收到的信息分别交给上面的应用层中的相应进程
运输层主要使用UDP和TCP两种协议,主要目的是将数据发送给给定的应用
二、UDP和TCP协议的特点及使用场景
UDP协议:
1、无连接
2、尽最大努力交付
3、面向报文
4、无拥塞控制
5、支持一对一,一对多,多对一,多对多的交互通信
6、首部开销小(四个字段:源端口,目的端口,长度,检验和)。UDP首部占用8个字节
UDP首部格式:
用户数据报UDP有两个字段:数据字段和首部字段。首部很简单,只有8字节。由四个字段组成,每个字段意义如下:
1、源端口:在需要对放回信时选用。不用时可用全0
2、目的端口:在终点交付报文时必须使用
3、长度:UDP用户数据报长度,其值最小为8(仅包含首部)
4、检验和:检验UDP用户数据报在传输中是否有错,有错就丢弃
TCP协议:
1、面向连接:通信之前必须要建立连接
2、每一条TCP只能点对点链接(一对一)
3、提供可靠的交付服务:通过TCP协议连接传输数据,无差错,不丢失,不重复
4、提供全双工通信
5、面向字节流。虽然程序和TCP交互是一次一个数据块,但是把应用程序交下来的数据仅仅看成的是一连串的无结构字节流
6、TCP首部占20字节
TCP首部格式:
各个字段的作用与含义:
1、源端口号和目的端口号
各占两个字节,我们知道端口号就是识别特定主机上的唯一进程,而ip地址是用来标识网络中的不同主机的,两个源(sourse)和目的(dst)端口号和IP首部中的源和IP地址,则标识互联网上的唯一进程,所以套接字的定义说白了就是:IP地址和端口号共同组成
2、确定序列
四个字节,上一个字段的序号是对数据的编号,所以确认序列号是下一个期望接受的TCP分段号,相当于对对方发送的并且已经被本方接收的分段确认。仅当ACK标志为1时有效。确认号表示期望收到的下一字节的序号
ack:期望下次收到的序号
ack怎么算的:通过收到的序号和数据长度相加。假设A收到B过来的数据(seq = 5,len = 15).len表示数据长度,那么A就会回复B,“刚才数据我收到了,你接下来就发序列号为20的包给我把”。这样就保证了不会乱序
3、ACK
当ACK为1时。确认号表示期望收到的下一字节的序号
4、序号
四个字节,表示在这个报文段中的第一个数据字节序号。如果将字节流看作两个应用程序的单向流动,则TCP用序号对每个字节进行计数。用来保证达到数据顺序的编号,所以这个字段是需要比加大的存储
5、位数据偏移
以32位(4字节)字长为单位,需要这个值是因为任选字段长度是可变的。跟IP头部一样,以4字节为单位。最大60字节。不存在任选字段正常的报头长度是20字节。其实相当于给出数据在数据段中的开始位置
6、保留位
6位,必须为0
7、标志位
占有6个比特位,他们中可以有多个位置为1,以此为:URG,ACK,PSH,RST,SYN,FIN
下面具体分析:
URG:该位置为1说明TCP包的紧急指针域有效,用来保证TCP链接不被中断,并且督促上层应用尽快处理这次数据
PSH:接收方会把这个报文尽快交给应用层,叫做push。所谓push操作就是把指在数据包到达接受端以后,立即传送给应用程序,而不是在缓冲区排队
8、RST
连接复位,复位因主机崩溃/别的原因出现的连接错误,也可以拒绝非法的分段请求
9、SYN
是一个同步序号,通常和ACK合用来建立连接,也就是通常说的三次握手
10、FIN
既然有建立连接那么必然有拆除连接,这个字段表示发送端已经到达数据末尾,也就是双方数据传送完成,没有数据传送,发送FIN标志位的TCP数据包后,连接断开。这个标志的数据包也经常用于进行端口扫描
UDP使用场景:
UDP由于不保证消息的可靠性,所以UDP适合发送一些不需要每条都保证无误的到达接收者
例如:视频通信
TCP使用场景:
由于TCP能够保证消息的可靠性,所以例如游戏信息,文字信息等适用于TCP通信
三、编程实践
1.UDP编程步骤
UDP使用数据报进行数据传输,没有客户端与服务器端之分,只有发送方与接收方,两者哪个先启动都不会报错,但是会出现数据丢包现象。发送的内容有字数限制,大小必须限制在64k以内
UDP客户端步骤:
1.构造DatagramSocket实例
2.通过DatagramSocket实例的send和receive方法收发DatagramPacket报文
3.调用DatagramSocket的close()方法关闭连接
。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;
/**
*
*/
public class UDPClient {
private DatagramSocket socket;//开发YDP的工具包
private DatagramPacket packet;//消息封装类
private DatagramPacket sendpacket;//消息封装类
private Scanner scanner;//控制台输出
private final int port = 5676;
private final String IP = "127.0.0.1";
public UDPClient() {
try {
socket = new DatagramSocket();
byte[]bytes = new byte[1024];
//packet底层封装字节数组
packet = new DatagramPacket(bytes,0,bytes.length);
scanner = new Scanner(System.in);
} catch (SocketException e) {
e.printStackTrace();
}
}
public void startUDPClient(){
//先给服务器发送消息
try {
while (true) {
System.out.println("请输入信息:");
String s = scanner.nextLine();
byte[] bytes = s.getBytes();
//packet底层封装字节数组
sendpacket = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getByName(IP), port);
socket.send(sendpacket);
if (s.equals("exit")){
break;
}
socket.receive(packet);
String str = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收到的服务器信息:" + str);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket!=null){
socket.close();
socket = null;
}
}
}
public static void main(String[] args) {
new UDPClient().startUDPClient();
}
}
UDP服务端步骤:
1.构造DatagramSocket实例,指定本地端口
。
2.通过DatagramSocket的send和receive方法接收和发送DatagramPacket
3.调用DatagramSocket的close()方法关闭
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Scanner;
/**
* 服务器端
*/
public class UDPServer {
private DatagramSocket socket;//开发YDP的工具包
private DatagramPacket packet;//消息封装类
private DatagramPacket sendpacket;//消息封装类
private Scanner scanner;//控制台输出
/**
* 端口号:网络传输,通过端口号把信息传输到特定的程序(进程)上
* 我们这里把端口号写死,因为我们每次运行时候,服务器端的端口号是不停的变的
* 结果就是:客户端不好连接到服务器端
*/
private final int port = 5676;
public UDPServer() {
try {
//将服务器绑定到特定端口
socket = new DatagramSocket(port);
byte[]bytes = new byte[1024];
//packet底层封装字节数组
packet = new DatagramPacket(bytes,0,bytes.length);
scanner = new Scanner(System.in);
} catch (SocketException e) {
e.printStackTrace();
}
}
//接受客户端发送的请求,作出反应,返回结果
public void startUDPServer(){
//接收消息
try {
while (true) {
socket.receive(packet);
String str = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收到的客户端信息:" + str);
if (str.equals("exit")){
break;
}
//根据接受的消息 作出反应 处理后结果返回给客户
System.out.println("请输入回复信息:");
String s = scanner.nextLine();
byte[] bytes = s.getBytes();
//packet底层封装字节数组
//socket 就是封装客户端发来的消息 它里面包含的方法可以让我们获取到客户端的
//的ip地址和端口号 然后底下就可以让我们把客户端消息发过去
sendpacket = new DatagramPacket(bytes, 0, bytes.length, packet.getAddress(), packet.getPort());
socket.send(sendpacket);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (socket!=null){
socket.close();
socket = null;
}
}
}
public static void main(String[] args) {
new UDPServer().startUDPServer();
}
}
2.TCP编程步骤
TCP是基于字节流的传输层通信协议,所以TCP编程是基于IO流编程。
对于客户端,我们需要使用Socket类来创建对象。对于服务器端,我们需要使用ServerSocket来创建对象,通过对象调用accept()方法来进行监听是否有客户端访问
TCP客户端步骤:
1.构建Socket实例,通过指定的服务器地址和端口建立连接
。
2.利用Socket实例包含的InputStream和OutputStream进行数据读写
。
3.操作结束后调用socket实例的close方法关闭连接
。
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class BIOCIient {
private Socket socket;
private Scanner scanner;
private final int port = 5676;
private final String IP = "127.0.0.1";
private BufferedWriter writer;
private BufferedReader reader;
private OutputStream outputStream;
private InputStream inputStream;
private volatile boolean flag = true;
public BIOCIient() {
scanner = new Scanner(System.in);
try {
socket = new Socket(IP, port); //请求和服务器建立连接
//TCP三次握手的起点 只要这个socket 建立成功说明连接建立成功
} catch (IOException e) {
e.printStackTrace();
}
}
public void startClient() {
try {
boolean closed = socket.isClosed();
if (!closed) {
outputStream = socket.getOutputStream();//负责发送消息
inputStream = socket.getInputStream();// 负责接收消息
writer = new BufferedWriter(new OutputStreamWriter(outputStream));
reader = new BufferedReader(new InputStreamReader(inputStream));
new Thread(new SendThread()).start();
while (flag) {
String str = reader.readLine();
System.out.println("接受到服务器的信息为 : " + str);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
inputStream.close();
outputStream.close();
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class SendThread implements Runnable {
@Override
public void run() {
try {
while (true) {
System.out.println("请输入信息:");
String s = scanner.nextLine();
writer.write(s + "\n");
writer.flush();
if (s.equals("exit")) {
flag = false;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new BIOCIient().startClient();
}
}
TCP服务端步骤:
1.构建一个ServerSocket实例,指定本地的端口,用于监听其连接请求
。
2.调用socket的accept()方法获得客户端的连接请求,通过accept()方法返回的socket实例,建立与客户端的连接
。
3.通过返回的socket实例来获得InputStream和OutputStream,进行数据的写入和读出
。
4.调用socket的close()方法关闭socket连接
。
//TCP协议代码
public class BIOServer {
private ServerSocket socket;//开发TCP的工具包
private final int port = 5676;
public BIOServer() {
try {
//将服务器绑定到特定端口上
socket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
public void startTCPServer() {
//接收客户端的请求
try {
while (true) { //不停的接收客户端请求
Socket clientSocket = socket.accept(); //阻塞方法 阻塞到连接建立上为止
//TCP 建立连接 三次握手 三次握手的终点
// clientSocket请求连接的用户一一对应
//后面讲使用该clientSocket进行和这个用户之间的通信
new Thread(new ServerHandlder(clientSocket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new BIOServer().startTCPServer();
}
}
ServerHandlder:
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ServerHandlder implements Runnable {
private Socket clientSocket;
private BufferedWriter writer;
private BufferedReader reader;
private OutputStream outputStream;
private InputStream inputStream;
private Scanner scanner;
public ServerHandlder(Socket clientSocket) {
scanner = new Scanner(System.in);
this.clientSocket = clientSocket;
try {
outputStream = clientSocket.getOutputStream();//负责发送消息
inputStream = clientSocket.getInputStream();// 负责接收消息
//字节转字符的过程
//文件传输
// BufferedOutputStream outputStream1 = new BufferedOutputStream(outputStream);
writer = new BufferedWriter(new OutputStreamWriter(outputStream));
reader = new BufferedReader(new InputStreamReader(inputStream));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true) {
String str = reader.readLine();
System.out.println("接受到客户端的信息为 : " + str);
if (str.equals("exit")) {
break;
}
System.out.println("请输入回复信息:");
String s = scanner.nextLine();
writer.write(s + "\n");
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
inputStream.close();
outputStream.close();
reader.close();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我们可以从上述UDP和TCP得到两者区别:
若想对socket编程结构有深入了解,请看这篇博客:基于TCP/IP和UDP协议的socket编程结构解析,写的很不错,点赞!