Socket编程

1. 什么是Socket

为了应用层和传输层能够交互,操作系统提供一些API给应用层,这些API可以把应用层的数据交给传输层,而这些API就是socket。
传输层中有很多协议,其中知名的就是TCP和UDP,因此操作系统提供了两个版本的API让应用层使用。

TCP和UDP的区别
  1. TCP是有连接,UDP无连接。(打电话是有连接的,要通信双方东建立好连接,才能通信;而发短信是无连接,不需要建立好连接)
  2. TCP是可靠传输,UDP是不可靠传输。(A给B发信息能够知道B有没有收到的是可靠传输)
  3. TCP是面向字节流,UDP是面向数据报。(一个以字节尾单位,一个以数据报尾单位)
  4. 都是全双工(类似于打电话交流,而半双工就类似于对讲机)
  5. 对于有可靠性要求的场景适用TCP,而对于可靠性要求不高,同时对传输效率要求很高的情况使用UDP

2. UDP的Socket编程

主要要掌握的两个类1. DatagramSocket 2. DatagramPacket

  1. DatagramSocket(网卡代言人)

DatagramSocket()


DatagramSocket(int port)


void receive(DatagramPacket p)


void send(DatagramPacket p)


void close()
创建UDP数据报套接字的Socket,绑定本机任意一个随机端口,一般用于客服端.


创建UDP数据报套接字的Socket,绑定本机指定端口,一般用于服务端


从此套接字接收数据报,如果没收到,就会阻塞等待


从此套接字发送数据报


关闭数据报套接字
socket本质上也是文件,socket对应网卡这个硬件设备,操作系统把网卡当作文件来管理,通过网卡发送数据,就是“写文件”,通过网卡接收数据,就是“读数据”

  1. DatagramPacket 数据报

DatagramPacket(byte[] buf, int length)


DatagramPacket(byte[]buf, int offset, int length,SocketAddress address)


InetAddress getAddress()


int getPort()


byte[] getData()
构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)


构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)。address指定目的主机的IP和端口


从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址


从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号


获取数据报中的数据

  1. 使用UDP实现回显服务器(复读机)
public class UDPEchoServer {
	private DatagramSocket socket = null;
	//服务器端要绑定端口,IP是本机,IP不用绑定
	public UDPEchoServer(int port) throws SocketException {
		socket = new DatagramSocket(port);
	}
	//启动服务器
	public void start() throws IOException {
		System.out.println("服务器启动");
		//服务器要随时待命
		while(true) {
			//1.读取请求
			DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//空的数据报
			//输出型参数,把请求写道requestPacket
			//阻塞等待直到收到请求
			socket.receive(requestPacket);
			//2.根据请求计算响应
			//把requestPacket转成字符串,方便打印
			String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
			String response = process(request);
			//3.把响应写回客服端,数据报要有客服端的地址
			//requestPacket.getSocketAddress() 客户端的地址IP,端口
			DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,
															   requestPacket.getSocketAddress());
			socket.send(responsePacket);
			//打印日志
			System.out.printf("[%s:%d] req: %s; resp: %s\n", requestPacket.getAddress().toString(),
							  requestPacket.getPort(), request, response);
		}
	}

	public String process(String request) {
		return request;
	}

	public static void main(String[] args) throws IOException {
		UDPEchoServer server = new UDPEchoServer(8080);
		server.start();
	}
}
public class UdpEchoClient {
	private DatagramSocket socket = null;
	//服务器的IP,端口先存起来,后面要用
	private String serverIp;
	private int serverPort;
	//客服端不需要手动指定端口,因为你不知道客服端哪些端口是开发的,哪些是被占用的
	//让操作系统自动分配一个空闲端口,客服端的IP是本机IP不用绑定
	public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
		socket = new DatagramSocket();
		//等会要转成是点分十进制,如127.0.0.1
		this.serverIp = serverIp;
		this.serverPort = serverPort;
	}
	public void start() throws IOException {
		Scanner scanner = new Scanner(System.in);
		while(true) {
			//用户输入内容
			System.out.println("->");
			String request = scanner.next();
			//构造一个UDP请求,包含内容,目的IP,目的端口
			//InetAddress.getByName(this.serverIp)把字符串IP转成点分十进制
			DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
															  InetAddress.getByName(this.serverIp), this.serverPort);
			socket.send(requestPacket);
			//3.从服务器读取UDP响应
			//空数据报
			DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
			//阻塞等待,直到收到响应
			socket.receive(responsePacket);//输出型参数,把响应写到responsePacket
			String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
			//把响应写到控制台
			System.out.println(response);
		}
	}

	public static void main(String[] args) throws IOException {
		UdpEchoClient client = new UdpEchoClient("127.0.0.1", 8080);
		client.start();
	}
}

流程图如下:
Socket编程_第1张图片

如何在idea中启动多个客户端

咱们能够实现多个客服端访问一个服务器
Socket编程_第2张图片

3.Tcp的Socket编程

  1. ServerSocket类(给服务端使用)

构造方法
ServerSocket(int port)
创建一个服务端流套接字Socket,绑定端口
普通方法
Socket accept()
接收客服端连接,返回一个服务端Socket,并阻塞等待
void close()
关闭socket

  1. Socket类(既给服务器用,又给客服端用)

构造方法
Socket(String host, int port)
创建一个客服端的Socket,并与对应的ip,端口的进程建立连接
普通方法
InetAddress getInetAddress()
返回对方的IP,端口
InputStream getInputStream()
OutputStream getOutStream()
返回此套接字的输入流
返回此套接字的输出流

  1. 实现TCP版本的回显服务器
public class TcpEchoServer {
	private ServerSocket listenSocket = null;
	//服务器端绑定端口
	public TcpEchoServer(int port) throws IOException {
		listenSocket = new ServerSocket(port);
	}
	public void start() throws IOException {
		System.out.println("服务器启动");
		//使用多线程,让多个服务端都能访问
		//使用线程池
		ExecutorService service = Executors.newCachedThreadPool();
		while(true) {
			//1. 使用accept()接受客服端的连接
			Socket clientSocket = listenSocket.accept();
			//2.处理客服端的连接,使用多线程,每个客服端连上来都分配一个新的线程负责处理
			service.submit(new Runnable() {
				@Override
				public void run() {
					try {
						processConnection(clientSocket);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}

	private void processConnection(Socket clientSocket) throws IOException {
		System.out.printf("[%s:%d] 客服端上线\n", clientSocket.getInetAddress().toString(),
						  clientSocket.getPort());
		//处理客服端请求
		//放到try里面,结束时自动关闭输入,输出流
		try(InputStream inputStream = clientSocket.getInputStream();
			OutputStream outputStream = clientSocket.getOutputStream()) {
			while(true) {
				//1.读取请求并解析
				Scanner scanner = new Scanner(inputStream);
				if(!scanner.hasNext()) {
					//读完了,断开连接
					System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
									  clientSocket.getPort());
					break;
				}
				String request = scanner.next();
				//2.根据请求计算响应
				String response = process(request);
				//3.将响应写回客服端
				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(), request, response);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}finally {
			//一个客服端就创建一个clientSocket,要关闭防止文件资源泄露
			//而listenSocket是全局只有一个,不用关闭,随着进制结束而结束
			clientSocket.close();
		}
	}

	private String process(String request) {
		return request;
	}

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

如何理解listenSocket和clientSocket

listenSocket通过accept方法和客服端建立连接,一个clientSocket处理一个客服端连接。以卖房子的中介为例,listenSocket就类似外场销售,负责在外面拉客(可以拉很多客人),而clientSocket就是内场销售,一个内场销售服务一个客人。

为什么需要关闭clientSocket的连接

clientSocket在循环中,每个客服端连接上来就要分配一个,这个对象就会被反复的创建出实例,每创建一个,都要消耗一个文件描述符,,因此需要及时释放防止文件资源泄露

为什么需要多线程,而UDP版本的不需要

当没有客服端连接时,服务器就会阻塞到accept,如果第一个客服端过来,此时就会进入processConnection方法,连接建立好了以后,但是客服端还没发消息,此时代码阻塞在hasNext。当第二个客服过来,无法调用accept,也就无法处理第二个客服端,所以需要多线程。
UDP是无连接的,客服端直接请求即可,不必专注处理某个客服端。而TCP建立连接后,要处理客服端多次请求,才导致无法快速调用到accept方法,这是长连接。但改成短连接时TCP就能快速调用到accept方法。

public class TcpEchoClient {
	//客服端使用Socket对象和服务端建立连接
	private Socket socket = null;
	public TcpEchoClient(String serverIp, int serverPort) throws IOException {
		//和服务端建立连接需要知道服务端的IP,Port
		socket = new Socket(serverIp, serverPort);
	}
	public void start() {
		Scanner scanner = new Scanner(System.in);
		try(InputStream inputStream = socket.getInputStream();
			OutputStream outputStream = socket.getOutputStream()) {
			while(true) {
				//1.从控制台读取数据,构造请求
				System.out.print("->");
				String request = scanner.next();
				//2.发送请求给服务器
				PrintWriter printWriter = new PrintWriter(outputStream);
				printWriter.println(request);
				printWriter.flush();
				//3.从服务器读取响应
				Scanner respScanner = new Scanner(inputStream);
				String response = respScanner.next();
				//4.把响应显示到界面上
				System.out.println(response);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	public static void main(String[] args) throws IOException {
		TcpEchoClient client = new TcpEchoClient("127.0.0.1", 6060);
		client.start();
	}
}

你可能感兴趣的:(网络,网络,socket编程)