TCP和UDP网络编程

Posted 专治八阿哥的孟老师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP和UDP网络编程相关的知识,希望对你有一定的参考价值。

TCP和UDP协议是TCP/IP协议的核心。 TCP 传输协议:TCP 协议是一TCP (Transmission Control
Protocol)和UDP(User Datagram
Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。通俗说,它是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送;而UDP则不为IP提供可靠性、流控或差错恢复功能。一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用

1. 基于TCP的网络编程

IP是一个通信实体在网络上的地址,通信实体可以是打印机、计算机等。IP协议的作用是让Internet成为一个允许连接不同类型计算机和不同操作系统的网络。IP协议负责将消息从一个主机传到另一个主机,消息在传递过程中被分割成小包。但是IP协议不能解决数据包在传输过程中遇到的问题,所以计算机需要TCP协议来保证传输的可靠性。
TCP叫端对端协议,当一台计算机要与另一台远程计算机进行通信时,TCP在两台计算机之间建立一个连接,用于发送和接收数据。TCP协议负责将数据包按一定顺序放好并发送,接收端在按照正确的顺序排列数据包。TCP提供了一个重发机制,当一个通信实体A发送消息给通信实体B后,A需要收到B的确认信息,如果没有收到B的确认信息,A会重新发送信息。
凡是在Internet上的计算机都必须安装IP协议和TCP协议,这两个协议统称TCP/IP协议。 TCP的三次握手
a.客户端向服务端发送一个请求 b.服务端收到请求后,回客户端一个响应 c.客户端向收到服务端的响应后,回服务端一个确认信息

1.1 ServerSocket创建服务端

ServerSocket用于监听来自客户端的Socket连接。如果没有连接,它将处于等待状态。 ServerSocket中常用方法:
Socket accept():如果接收到一个客户端Socket连接请求,该方法返回一个客户端对应的Socket对象。
ServerSocket(int port):用户指定端口port来创建一个ServerSocket,端口在1~65535之间。
close():用于关闭服务端。

public class Server 
	public static void main(String[] args) 
		// 1.实例化一个ServerSocket的对象
		ServerSocket serverSocket = null;
		OutputStream output = null;// 用于向别人发消息
		InputStream input = null;// 用于读取别人发过来的消息
		try 
			serverSocket = new ServerSocket(8888);
			System.out.println("等待服务端的连接...");
			// 2.监听,获取连接到的客户端 注意:在未连接成功之前,将一直处于阻塞状态
			Socket socket = serverSocket.accept();
			System.out.println("连接成功");
			// 3.获取网络到内存的一个输入流
			input = socket.getInputStream();
			// 4.读取数据,这个数据是Client发过来的
			byte[] arr = new byte[1024];
			int len = input.read(arr);
			String message = new String(arr, 0, len);
			// 5.组织信息
			String ipString = socket.getInetAddress().getHostAddress();
			int port = socket.getPort();
			System.out.println(ipString + ":" + port + "说:" + message);
			// 6.服务端给客户端回复消息
			output = socket.getOutputStream();
			output.write("你也好,好久不见了".getBytes());
			output.flush();
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			try 
				output.close();
				input.close();
				serverSocket.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

服务端不应该只接受一个客户端请求后就停止,所以在程序中可以通过循环不断的调用accept()方法:

while(true)
Socket socket=serverSocket.accept();

1.2 Socket创建客户端

客户端可以使用Socket构造器连接指定的服务端。

public class Client 
	public static void main(String[] args) 
		Socket socket = null;// 1.建立一个与服务端之间的连接
		OutputStream output = null;// 用于向别人发消息
		InputStream input = null;// 用于读取别人发过来的消息
		try 
			socket = new Socket("127.0.0.1", 8888);
			// 2.将需要发送的数据写入到网络中,注意:包含了两个流:InputStream和OutputStream
			output = socket.getOutputStream();
			output.write("hello你好吗?".getBytes());// 3.写入
			output.flush();
			// 4.收取服务端发送来的消息 new BufferedInputStream(socket.getInputStream());
			input = socket.getInputStream();
			byte[] arr = new byte[1024];
			int len = input.read(arr);
			String message = new String(arr, 0, len);
			System.out.println("来自服务端的回复:" + message);
		 catch (UnknownHostException e) 
			e.printStackTrace();
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			try 
				input.close();
				output.close();
				socket.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

1.3 加入多线程

使用多线程实现客户端和服务端持续会话:

public class ServerThread extends Thread 
	private Socket client;

	public ServerThread() 
	

	public ServerThread(Socket client) 
		this.client = client;
	

	@Override
	public void run() 
		OutputStream output = null;
		InputStream input = null;
		try 
			output = client.getOutputStream();
			input = client.getInputStream();
			// 从控制台进行获取数据
			Scanner scanner = new Scanner(System.in);
			// 进行循环的发送和接收消息
			while (true) 
				byte[] arr = new byte[1024];// 接收客户端发送来的消息
				int len = input.read(arr);
				String message = new String(arr, 0, len);
				System.out.println("来自客户端的消息:" + message);
				System.out.println("服务端对客户端说:");
				String reply = scanner.nextLine();// 回复消息
				output.write(reply.getBytes());
				output.flush();
				if (message.equals("886") || message.equals("bye") || message.equals("再见")) // 约定
					break;
				
			
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			try 
				input.close();
				output.close();
				client.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

public class Server 
	public static void main(String[] args) 
		ServerSocket serverSocket = null;
		try 
			serverSocket = new ServerSocket(8888);
			while (true) 
				Socket socket = serverSocket.accept();
				ServerThread thread = new ServerThread(socket);
				thread.start();		
			
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			try 
				serverSocket.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

public class Client 
	public static void main(String[] args) 
		Socket socket = null;// 1.建立一个与服务端之间的连接
		OutputStream output = null;// 用于向别人发消息
		InputStream input = null;// 用于读取别人发过来的消息
		Scanner scanner = new Scanner(System.in);
		try 
			socket = new Socket("127.0.0.1", 8888);
			output = socket.getOutputStream();
			input = socket.getInputStream();
			while (true) 
				System.out.println("客户端对服务端说:");
				String message = scanner.nextLine();
				output.write(message.getBytes());// 进行发送
				output.flush();
				byte[] arr = new byte[1024];// 接收消息
				int len = input.read(arr);
				String reply = new String(arr, 0, len);
				System.out.println("收到服务端的反馈:" + reply);
				if (reply.equals("886") || reply.equals("bye") || reply.equals("再见")) // 约定
					break;
				
			
		 catch (UnknownHostException e) 
			e.printStackTrace();
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			try 
				input.close();
				output.close();
				socket.close();
			 catch (IOException e) 
				e.printStackTrace();
			
		
	

2. 基于UDP的编程

UDP是User Datagram
Protocol的简称,用户数据报协议,它不管对方是否在线,直接向对方发送数据,至于对方能否收到,UDP无法控制,所以它是一种简单不可靠的协议。适用于一次传输量较少,对可靠性要求不高的情景。

特点:
a.不安全
b.无连接
c.效率高
d.UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内

public class Receiver 
	public static void main(String[] args) 
		// 1.实例化DatagramSocket的对象,需要进行绑定端口号:由发送方发送来的端口号进行决定
		DatagramSocket socket = null;
		try 
			socket = new DatagramSocket(6666);
			// 2.将接收到的数据封装到数据报包中,用来接收长度为 length 的数据包。
			byte[] arr = new byte[4096];
			DatagramPacket packet = new DatagramPacket(arr, arr.length);
			System.out.println("等待接收数据~~~~~~~");
			// 3.接收数据
			// 注意:将数据从网络中读取出来
			socket.receive(packet);
			// 4.获取发送方的详细信息
			byte[] messages = packet.getData();
			String result = new String(messages, 0, packet.getLength());
			// 获取发送方的ip地址
			// 返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。
			InetAddress address = packet.getAddress();
			String ip = address.getHostAddress();
			// 获取消息是从发送发的哪个端口号发出来的
			int port = packet.getPort();
			System.out.println(ip + ":" + port + "说:" + result.trim());
		 catch (SocketException e) 
			e.printStackTrace();
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			socket.close();
		
	

public class Sender 
	public static void main(String[] args) 
		// 端口号表示的是指定的接收方的端口号,而发送方的端口是由系统自动分配的
		sendMessage("127.0.0.1", 6666, "你好啊");
	

	public static void sendMessage(String ip, int port, String message) 
		// 1.实例化DatagramSocket的对象, 注意:和流的使用类似,使用套接字完成之后需要关闭
		DatagramSocket socket = null;
		try 
			socket = new DatagramSocket();
			// 2.构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
			DatagramPacket packet = new DatagramPacket(message.getBytes(), message.getBytes().length, InetAddress.getByName(ip),
					port);
			// 3.发送,将数据写入网络的过程,void send(DatagramPacket p) 从此套接字发送数据报包。
			socket.send(packet);
		 catch (SocketException e) 
			// 父类为IOException
			e.printStackTrace();
		 catch (UnknownHostException e) 
			e.printStackTrace();
		 catch (IOException e) 
			e.printStackTrace();
		 finally 
			socket.close();
		
	

传输控制协议(TCP):TCP(传输控制协议)定义了两台计算机之间进行可靠的传输而交换的数据和确认信息的格式,以及计算机为了确保数据的正确到达而采取的措施。协议规定了TCP软件怎样识别给定计算机上的多个目的进程如何对分组重复这类差错进行恢复。协议还规定了两台计算机如何初始化一个TCP数据流传输以及如何结束这一传输。TCP最大的特点就是提供的是面向连接、可靠的字节流服务。
用户数据报协议(UDP):UDP(用户数据报协议)是一个简单的面向数据报的传输层协议。提供的是非面向连接的、不可靠的数据流传输。UDP不提供可靠性,也不提供报文到达确认、排序以及流量控制等功能。它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。因此报文可能会丢失、重复以及乱序等。但由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。

Java网络编程之UDP和TCP套接字

文章目录

一. 网络编程概述

我们知道在网络通信中, 数据的发送是从应用层开始, 一直封装到物理层然后进行发送的, 应用层要将数据交给传输层进行封装; 而接收方拿到数据后是从物理层到应用层进行分用, 传输层要将拿到的数据再分用给应用层进行使用, 网络编程实际操作中最关键的就是我们所能控制的应用层和传输层之间的交互, 而在操作系统中提供了一组API即socket, 用来实现应用层和传输层之间的交互, Java当中把操作系统提供的API进行了进一步封装以便我们进行使用.

常见传输层协议有UDP和TCP两种, 其中UDP的特点是无连接, 不可靠传输, 面向数据报, 全双工; TCP的特点是有连接, 可靠传输, 面向字节流, 全双工.

使用TCP协议, 必须是通信双方先建立连接才能进行通信(想象打电话的场景), 而使用UDP协议在无连接的情况下可以进行通信(想象发微信, 短信的场景).

这里的可靠与不可靠传输指的不是安全性质, 而是说你发送出数据后, 能不能判断对方已经收到, 如果能够确定对方是否收到则就是可靠传输, 否则就是不可靠传输.

面向字节流就类文件读写数据的操作, 是 “流” 式的; 而面向数据报的话数据传输则是以一个个的 “数据报” 为基本单位(一个数据报可能是若干个字节, 是带有一定的格式的).

全双工是指一条通信链路, 可以双向传输(同一时间既可以发, 也可以收); 而半双工是一条链路, 只能单向通信.

二. UDP网络编程

1. UDP套接字

UDP类型的Socket, 涉及两个核心类, 一个是DatagramSocket, 其实例的对象表示UDP版本的Socket, 操作系统中将网卡这种硬件设备也抽象成了文件进行处理, 这个Soket对象也就成了文件描述表上面的一项, 通过操作这个Socket文件来间接的操作网卡, 就可以通信了.

有一个Socket对象就可以与另一台主机进行通信了, 但如果要和不同的主机通信, 就需要创建多个Socket对象, 使用DatagramSocket既可以发, 也可以收, 体现了UDP全双工的特点.

构造方法作用
DatagramSocket()创建一个UDP数据报套接字的Socket, 绑定到任意一个随机端口号(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket, 绑定到本机指定端口(一般用于服务器)

关于这两个方法也是好理解的, 一般服务器的端口号需要自己指定, 如果随机分配的话, 你的客户端要怎么访问你的服务器呢?毕竟客户端才是主动的一方, 知道服务器在哪里才能找到它进行通信吧.

关键方法作用
void receive(DatagramPacket p)从此套接字接收数据 (如果没有接收到数据报, 进行阻塞等待)
void send(DatagramPacket p)从此套接字发送数据包 (不会阻塞等待,直接发送)
void close()关闭此数据报套接字

这里receive方法参数传入的是一个空的对象, receive方法内部会对这个对象进行填充, 从而构造出结果数据, 这个参数也是一个输出型参数.

第二个是DatagramPacket, 表示一个UDP数据报, 在UDP的服务器和客户端都需要使用到, 接收和发送的数据就是在传输DatagramPacket对象, 这就是体现了UDP面向数据报的特点.

构造方法作用
DatagramPacket(byte[] buf, int length)构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组里, 接受指定长度
DatagramPacket(byte[] buf, int offset,int length, SocketAddress address)构造一个DatagramPacket用来发送数据报,发送的数据为字节数据,从0到指定长度,address用来指定目的主机的IP和端口号
关键方法作用
InetAddress getAddress()从接受的数据报中,获取发送端IP地址,或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号,或从发送的数据报中,获取接收端的端口号
SocketAddress getSocketAddress()从接收的数据报中,获取发送端主机SocketAddress,或从发送的数据报中,获取接收端的SocketAddress(IP地址+端口号)
byte[] getData()获取数据报的数据

在网络编程中, 一定要注意区分清楚服务器与客户端使用之间使用的五元组, 具体如下:

  1. 源IP, 就是发送方IP.
  2. 源端口, 发送方端口号, 服务器需要手动指定, 客户端让系统随机分配即可.
  3. 目的IP, 接收方IP, 包含在拿到的数据报中, 服务器响应时的目的IP就在客户端发来的数据报中, 客户端发送请求时的目的IP就是服务器的IP.
  4. 目的端口, 接收方端口号包含在数据报中, 服务器响应时的目的端口就在客户端发来的数据报中, 客户端发送请求时的目的端口就是服务器的端口号.
  5. 协议类型, 如UDP/TCP.

2. UDP客户端回显服务器程序

正常来说, 客户端和服务器程序要实现的是, 客户端发送请求, 服务器接收请求后, 要根据请求计算响应(业务逻辑), 然后把响应返回给客户端.

而在这里只是要演示Socket api的用法, 就不涉及业务逻辑了, 我们让服务器收到什么就给客户端返回什么, 这样实现服务器就叫做回显服务器.

2.1 UDP回显服务器

UDP服务器设计步骤:

  1. 创建Socket实例对象(DatagramSocket对象), 需要指定服务器的端口号, 因为服务器是被动接收和处理请求的一端, 而客户端是主动发起请求的一端, 客户端必须得知道服务器在哪里才能发送请求, 也就是需要知道服务器的端口号.
  2. 服务器启动, 读取客户端请求, 把得到的数据填充到DatagramPacket对象中, 这里得到的请求中是包含着有关客户端的地址信息的(IP+端口号), 可以通过getSocketAddress方法获取到.
  3. 处理客户端请求, 计算响应, 这里实现的是一个回显服务,直接根据请求返回相同的响应即可, 但在实际开发中, 这个处理请求的部分其实是最关键的.
  4. 将响应返回给客户端, 要将响应数据构造成DatagramPacket对象, 注意要给出客户端的地址信息.

代码如下:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// UDP版本的回显服务器
public class UdpEchoServer 

    //准备好socket实例,准备传输
    private DatagramSocket socket = null;

    //构造时指定服务器的端口号
    public UdpEchoServer(int port) throws SocketException 
        this.socket = new DatagramSocket(port);
    

    public void start() throws IOException 
        System.out.println("服务器启动!");
        //服务器要给多个客户端提供服务
        while (true) 
            //1. 读取客户端发过来的请求
            DatagramPacket requstPacket = new DatagramPacket(new byte[4096], 4096);
            //receive内部会对参数对象进行填充数据,填充的数据来源于网卡
			socket.receive(requstPacket);
            //解析收到的数据包,一般解析成字符串进行处理; 构造字符串的参数分别为数据数组,存入数据数组的起始下标,长度
            String requst = new String(requstPacket.getData(), 0, requstPacket.getLength());
            //2. 根据请求计算响应
            String response = process(requst);
            //3. 把响应写回到客户端中
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requstPacket.getSocketAddress());
            socket.send(responsePacket);
            //输出一下发送日志
            System.out.printf("[%s:%d] req: %s, resp: %s \\n", requstPacket.getAddress().toString(),
                    requstPacket.getPort(), requst, response);
        
    

    public String process(String requst) 
        //...处理数据,这里是回显服务直返回原数据即可
        return requst;
    

    public static void main(String[] args) throws IOException 
        // 1024 到 65535即可
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    

上面代码中要注意

这里response.getBytes().length要注意不能写成response.length(), 因为DatagramPacket不认字符只认字节, response.length()获取的是有效字符数, response.getBytes().length获取的是有效字节数.

代码中的循环是一个死循环, 这样设施也是没有问题的, 大部分服务器都是要7 * 24小时运行的.

2.2 UDP客户端

UDP客户端设计步骤:

  1. 创建Socket实例对象(DatagramSocket对象), 可以指定端口号创建也可以让系统随机分配, 自己指定端口号容易与已经被使用的端口号冲突, 所以这里让所以系统随机分配更就行了, 不用担心端口号的冲突的问题.
  2. 用户输入请求, 并使用DatagramPacket对象打包数据请求, 要注意给出服务器的地址(IP和端口号), 并将请求发送.
  3. 读取服务器返回的响应并进行处理.

代码如下:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

//UDP版本的回显客户端
public class UdpEchoCliet 
    private DatagramSocket socket = null;
    //需要指定服务器的ip和端口号
    private String serverIp = null;
    private int serverPort = 0;

    public UdpEchoCliet(String serverIp, int serverPort) throws SocketException 
        //让系统分配一个空闲的端口号即可
        this.socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    

    public void start() throws IOException 
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true) 
            // 1. 从控制台读取要发送的数据
            System.out.print("> ");
            String request = scanner.next();
            if (request.equals("exit")) 
                System.out.println("客户端退出");
                break;
            
            // 2. 构造UDP请求,并发送
            //上面的IP地址是一个字符串,需要使用InetAddress.getByName来转换成一个整数.
            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);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 把解析好的结果显示出来
            System.out.println(response);
        
    

    public static void main(String[] args) throws IOException 
        //在本机上测试,127.0.0.1是IP,表示自己主机
        UdpEchoCliet cliet = new UdpEchoCliet("127.0.0.1", 9090);
        cliet.start();
    

上面说到, 为了防止客户端端口号的冲突, 我们让系统为客户端随机分配了端口, 那么客户端为什么就不怕端口号冲突呢?

其实也好理解, 服务器肯定是程序员自己手里的机器, 上面运行啥, 程序员就可以安排哪个程序使用哪个端口, 也就是说, 服务器上面的程序是可控的; 而客户端是运行在用户电脑上的, 环境复杂, 不可控性高.

对于客户端服务器程序来说一个服务器是要给许多客户端提供服务的, 但是IDEA默认只能启动一个客户端, 要想测试多个客户端, 需要我们手动设置一下IDEA.

第一步, 右键代码编辑处, 按下图进行操作.

第二步, 找到Modify options并点击.

第三步, 勾选上Allow multiple instances后, 点击OK即可.

测试结果:

首先一定要先启动服务, 然后再启动客户端进行测试.

2.3 UDP实现查词典的服务器

上面实现的回显服务器缺乏业务逻辑, 这里在上面的代码的基础上稍作调整, 实现一个 “查词典” 的服务器(将英文单词翻译成中文解释), 这里其实就很简单了, 对于客户端的代码还可以继续使用, 服务器只需把处理请求部分的代码修改即可, 我们可以继承上面的回显服务器, 重写请求部分的代码, 英语单词和汉语解释可以由一个哈希表实现映射关系, 构成词库, 然后根据请求来获取哈希表中对应的汉语解释即可.

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer
    private Map<String, String> dict = new HashMap<>();
    public UdpDictServer(int port) throws SocketException 
        super(port);
        //词库
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("bird", "小鸟");
        dict.put("apple", "苹果");
        dict.put("banana", "香蕉");
        dict.put("strawberry", "草莓");
        dict.put("watermelon", "西瓜");
        //...
    
    @Override
    public String process (String request) 
        //查词典
        return dict.getOrDefault(request, "当前单词没有查到结果!");
    

    public static void main(String[] args) throws IOException 
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    

关于网络编程这里涉及一个重要的异常, 如下图:

这是一个表示端口冲突的异常, 一个端口只能被一个进程使用, 如果有多个进程使用同一个端口, 就会出现如上图的异常.

三. TCP网络编程

1. TCP套接字

TCP相比于UDP有很大的不同, TCP的话首先需要通信双方成功建立连接然后才可以进行通信, TCP进行网络编程的方式和文件读写中的字节流类似, 是字节为单位的流式传输, 如果对下面涉及的IO流操作不熟悉的话, 可以看一看我前面的一篇博客 Java文件IO操作及案例 .

对于TCP的套接字, Java提供了两个类来进行数据的传输, 一个是ServerSocket, 是专门给服务器使用的Socket对象, 用来让服务器接收客户端的连接;

构造方法解释
ServerSocket(int port)创建一个服务器套接字Socket,并指定端口号
关键方法解释
Socket accept()开始监听指定端口,有客户端连接时,返回一个服务端Socket对象,并基于该Socket对象与客户端建立连接,否则阻塞等待
void close()关闭此套接字

第二个是Socket, 这个类在客户端和服务器都会使用, 进行服务器与客户端之间的数据传输通信, TCP的传输可以类比打电话的场景, 客户端发送请求后, 服务器调用ServerSocket类的accept方法来 “建立连接” (接通电话), 建立连接后两端就可以进行通信了, Socket可以获取到文件(网卡)的输入输出流对象, 然后就可以流对象进行文件(网卡)读写了, 体现了TCP面向字节流, 全双工的特点.

构造方法解释
Socket(String host,int port)创建一个客户端Socket对象,并与对应IP主机,对应端口的进程进行连接
关键方法解释
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回套接字的输入流
OutputStream getOutputStream()返回套接字的输出流

TCP的通信需要建立连接, 这里涉及长短连接的问题, 什么时候关闭连接就决定了是短连接还是长连接, 具体如下:

  • 短连接: 每次接收到数据并返回响应后, 都关闭连接, 即是短连接; 也就是说, 短连接只能一次收发数据.
  • 长连接: 不关闭连接, 一直保持连接状态, 双方不停的收发数据, 即是长连接; 也就是说, 长连接可以多次收发数据.

对比以上长短连接,两者区别如下:

  • 建立连接, 关闭连接的耗时: 短连接每次请求, 响应都需要建立连接, 关闭连接; 而长连接只需要第一次建立连接, 之后的请求, 响应都可以直接传输; 相对来说建立连接, 关闭连接也是要耗时的, 长连接效率更高.
  • 主动发送请求不同: 短连接一般是客户端主动向服务端发送请求, 而长连接可以是客户端主动发送请求, 也可以是服务端主动发.
  • 两者的使用场景有不同: 短连接适用于客户端请求频率不高的场景, 如浏览网页等; 长连接适用于 客户端与服务端通信频繁的场景, 如聊天室, 实时游戏等.

2. TCP客户端回显服务器程序

2.1 TCP回显服务器

TCP服务器设计步骤:

  1. 创建ServerSocket实例对象, 需指定服务器端口号.
  2. 启动服务器, 使用accept方法和客户端建立连接, 如果没有客户端来连接, 这里的accept方法会阻塞.
  3. 接收客户端发来的请求(通过Socket获取到InputStream流对象来读取请求).
  4. 处理客户端请求, 计算响应.
  5. 将响应返回给客户端(通过Socket获取到OutputStream流对象来发送响应).
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//TCP版本的回显服务器
public class TcpEchoServer 
    //服务器专用的socket,用来和客户端建立连接
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException 
        serverSocket = new ServerSocket(port);
    
    //启动服务器
    public void start() throws IOException 
        System.out.println("启动服务器!");
        while (true) 
            //和客户端建立连接
            Socket clientSocket = serverSocket.accept();
            //和客户端进行交互,
            //这个写法只有一个线程,同一时间只能处理一个客户端
            proccessConnection(clientSocket);
        
    

    //处理一个客户端连接
    private void proccessConnection(Socket clientSocket) 
        System.out.printf("[%s:%d] 客户端端上线!\\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        //基于clientSocket对象和客户端进行通信
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) 
            //客户端可能有多个请求,所以使用循环来处理
            while (true) 
                // 1. 读取请求
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) 
                    //hasNext()方法判断输入(文件,字符串,键盘等输入流)是否还有下一个输入项,若有,返回true,反之false.
                    //hasNext会等待客户端那边的输入,即会阻塞等待输入源的输入
                    //当客户端那边关闭了连接,输入源也就结束了,没有了下一个数据,说明读完了,此时hasNext()就为false了
                    System.out.printf("[%s:%d] 客户端下线!\\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                
                //next 是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含空白符.
                String requst = scanner.next();
                // 2. 根据强求构造响应
                String  response = process(requst);
                // 3. 返回响应的结果
                outputStream.write(response.getBytes(), 0, response.getBytes().length);
                byte[] blank= '\\n';
                outputStream.write(blank);
                outputStream.flush();

                //或者使用println来写入,让结果中带有一个 \\n 换行,方便对端来接收解析.
                /*PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //刷新缓冲区,保证当前写入的数据发送出去
                printWriter.flush();*/

                System.out.printf("[%s:%d] req: %s; resp: %s \\n", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), requst, response);
            
         catch (IOException e) 
            e.printStackTrace();
         finally 
            try 
                //释放资源,相当于挂断电话
                clientSocket.close();
             catch (IOException e) 
                throw new RuntimeException(e);
            
        
    

    public String process(String requst) 
        return requst;
    

    public static void main(String[] args) throws IOException 
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    

要注意理解这里的代码,

这里的 scanner.hasNext() 什么时候会使false呢? 这是因为, 当客户端退出之后, 对应的流对象就读到了EOF(文件结束标记), 那这里为啥会读到EOF, 这是因为客户端进程退出的时候, 就会触发socket.close(), 也就触发FIN(客户端端关闭连接的请求, 这个涉及到TCP协议连接管理的知识), 也就是操作系统内核收到客户端方发来的FIN数据报, 就会将输入源结束, 标记为EOF.

上面实现的TCP回显服务器的代码中有一个致命的缺陷就是, 这个代码同一时间只能连接一个客户端, 也就是只能处理一个客户端的请求, 下面先写客户端的代码, 然后再分析这里的问题.

2.2 TCP客户端

TCP客户端设计步骤:

  1. 创建Socket实例对象, 用于与服务器建立连接, 参数为服务器的IP地址和端口号, 在new Socket实例对象的时候, 就会触发和TCP的连接过程.
  2. 客户端启动, 用户输入请求, 构造构造请求并发送给服务器(使用OutputStream/PrintWriter), 要注意去刷新缓冲区保证数据成功写入网卡.
  3. 读取服务器的响应并进行处

    以上是关于TCP和UDP网络编程的主要内容,如果未能解决你的问题,请参考以下文章

    TCP和UDP网络编程

    TCP与UDP的区别

    UDP千兆网之TCP/IP协议基础

    TCP和UDP的区别

    UDP和TCP协议之间的联系与区别及他们与IP协议的联系是啥?要详细的

    TCP和UDP的区别