目录
一、什么是Socket套接字
二、UDP数据包套接字编程
1.DatagramSocket API
(1)关于Socket对象
(2)DatagramSocket方法
2. DatagramPacket API
DatagramPacket方法
3.基于UDP Socket的客户端服务器程序(回显服务器echo server)
4.单词翻译服务器
三、TCP数据包套接字编程
1.SeverSocket API
2.Socket API
3.基于TCP套接字的回显程序
数据报,Datagram,通过网络传输的数据的基本单元,包含一个报头(header)和数据本身,其中报头描述了数据的目的地以及和其它数据之间的关系。
概念:socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
TCP/IP五层网络模型:应用层,传输层,网络层,数据链路层,物理层。其中应用层主要是应用程序,传输层和网络层是是由系统内核封装的,数据链路层和物理层主要是由硬件和驱动实现的。在网络分层下,数据的传输离不开封装和分用。
程序员写网络程序 ,主要编写的是应用层代码,其他下面四层是程序员无法改变的。当应用程序需要将数据上传,此时就需要上层协议,调用下层协议,应用层调用传输层,传输层给应用层提供一组api,这组api就是套接字socket。
系统主要给我们提供两组Socket api:
1.基于UDP的api ;
2.基于TCP的api。
UDP协议和TCP协议的特点
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
有/无连接:使用UDP/TCP通信的双方,各自是否需要刻意保存对端的相关信息;
可靠传输:信息发出去,尽可能的传输过去。
不可靠传输:信息发出去,不关注结果,不关注是否传输过去。
面向数据报:以一个UDP数据报为传输的基本单位。
面向字节流:以字节流为传输的基本单位,读写方式十分灵活。
全双工/半双工:一条路径双/单向通信。
主要提供了两个类:DatagramSocket(Socket对象),DatagramPacket(udp数据报)
关于“报”,是网络传输数据的基本单位,这些基本单位主要包括:报(datagram)(udp中使用),包(packet)(ip中使用),段(segment)(tcp中使用),帧(frame)(数据链路层中使用)。日常生活中,不会特意区分这些单位,但是写研究论文需要区分。
Socket对象:相当于对应到系统中的一个特殊文件(socket文件),这个文件并非对应到硬盘上的某个数据存储区域,而是对应到网卡这个硬件设备。进行网络通信,离不开socket文件这样的对象,借助socket文件对象,才能间接的操作网卡(相当于遥控器)。
向socket对象中写数据,相当于通过网卡发送消息;向socket对象中读数据,就相当于通过网卡接收消息。
下图片的以太网适配器就是一个有线网卡:
无线网卡:
没有网卡就不能上网,一般是集成在主板上的。
文件:广义上,代指很多计算机中的软件/硬件资源;狭义上,代指硬盘上的一块数据存储区域。
DatagramSocket是UDP Socket用于发送和接收UDP数据报。
DatagramSocket构造方法:绑定一个端口号(服务器),也可以不显示指定客户端
方法签名 |
方法说明 |
DatagramSocket |
创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端) |
DatagramSocket(int port) |
创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端) |
DatagramSocket(int port):此处Socket对象可能被客户端/服务器使用,服务器的socket往往需要关联一个具体的端口号(不变);客户端这里不需要手动指定,系统自动分配即可(可以改变)。
DatagramSocket方法:
方法签名 |
方法说明 |
void receive(DatagramPacket p) |
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待) |
void send(DatagramPacket p) |
从此套接字发送数据报包(不会阻塞等待,直接发送) |
void close() |
关闭此数据报套接字,与文件操作一样,用完一定要关闭,否则会出现文件泄露问题 |
DatagramPacket是UDP Socket发送和接收的数据报。
DatagramPacket构造方法:
方法签名 |
方法说明 |
DatagramPacket(byte[] buf, int length) |
构造一个DatagramPacket,不需要设置地址进去,主要以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) |
构造一个DatagramPacket,显示地址设置,通常以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length),address指定目的主机的IP和端口号 |
DatagramPacket方法:
方法签名 |
方法说明 |
InetAddress getAddress() |
从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取 接收端主机IP地址 |
int getPort() |
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获 取接收端主机端口号 |
byte[] getData() |
获取数据报中的数据 |
作用:客户端发送一个请求,服务器返回一个一模一样的响应。没有什么实际作用,只保留了最核心的发送接收环节。
服务器的三个核心工作:
读取请求并解析
根据请求计算响应
把相应返回到客户端
整个回显服务器执行流程:
客户端读取用户输入
客户端构造请求,并发送
服务器读取用户请求数据
服务器根据请求计算响应
服务器把响应写回到客户端
客户端读取服务器的响应
客户端把响应转成字符串,并显示出来
以下代码执行顺序:
服务器先启动,执行到receive进行阻塞
客户端运行之后,从控制台读取数据,并进行send
此时客户端和服务器同时进行。客户端这边,send以后,继续往下执行,在receive里读取响应,读取以后会阻塞等待;服务器这边,就从receive返回,读取到请求数据(从客户端来的),往下走到process生成响应,然后再往下走到send并打印日志
客户端这边,当收到send回到的数据以后,就会接触阻塞,进行打印操作;服务器这边进入下一顿循环,再次阻塞到receive这里
客户端继续进行下一轮循环,阻塞在scanner.next这里等待用户执行新的内容
具体代码如下:
(1)服务器程序
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;//定义一个socket对象
//绑定一个端口,不一定能成功
//某个端口已经被别的进程占用了,此时绑定操作就会出错
//同一个主机上,一个端口,同一时刻,只能被一个进程绑定
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);
//从网卡中读取数据
//一旦服务器start,就会立即执行receive,如果此时客户端没有发送数据,此时的receive就是阻塞状态
socket.receive(requestPacket);
//将数据包(二进制)转换成string,处理此请求
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2.根据请求计算响应(省略)
String response = process(request);
//3.把响应结果写回到客户端
//根据response字符串。构造一个DatagramPacket
//与请求packet不同,此处构造
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
//requestPacket是从客户端收来的,getSocketAddress就会得到客户端的ip和端口
,requestPacket.getSocketAddress());
//客户端send请求,服务器receive请求
//服务器send响应,客户端receive响应
socket.send(responsePacket);
System.out.printf("[%s:%D]req:%s,resp:%s\n",responsePacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//此方法作用是根据请求计算响应
//回显程序中没有写具体的请求和回应
//如果写别的程序就可以修改process方法,根据需求重新构造响应
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
udpEchoServer.start();
}
}
(2)客户端程序
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.println("->");
String request = scanner.next();
//2.把字符串构造成UDP packet,并进行发送
//这个构造,就是把数据构造成DatagramPacket,一方面需要String中的getBytes数组,另一方面,需要指定服务器的ip和端口
//此处不是通过inetAddress直接构造的,而是分开设置的,一方面设置字符串的ip,一方面设置端口号
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,requestPacket.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();
}
}
对于UdpEchoServer来说,socket对象出循环就结束了生命周期,循环结束,意味着start结束,意味着main方法结束,即进程结束,当进程技结束以后,所有的文件资源就自动释放了,所以上述代码就不必显示调用close方法。
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
//使用继承,复用前面写过的代码
public class UdpDictServer extends UdpEchoServer{
//使用一个hash表,存储单词
//翻译的本质就是查表
private Map dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
//向表中添加元素
dict.put("dog","小狗");
dict.put("cat","小猫");
dict.put("fuck","卧槽");
}
//根据请求计算响应
@Override
public String process(String request) {
return dict.getOrDefault(request,"单词没有查到");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}
ServerSocket构造方法:构造是指定一个具体端口,让服务器绑定该端口。
方法签名 |
方法说明 |
ServerSocket(int port) |
创建一个服务端流套接字Socket,并绑定到指定端口 |
ServerSocket方法:
accept()方法就是接受,服务器是被动接受的一方,客户端是主动接受的一方
方法签名 |
方法说明 |
Socket accept() |
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待 |
Void close() |
关闭此套接字 |
Socket 构造方法:服务器IP和端口号,在客户端new Socket对象的时候,就会尝试和指定的ip端口的目标建立连接
方法签名 |
方法说明 |
Socket(String host, intport) |
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接 |
Socket方法:面向字节流,通过Socket对象拿到字节流对象,就可以通过字节流对象进行数据传输了,从InputStream里面读取数据,就相当于从网卡接收;在OutPutStream里面写数据,就相当于从网卡发送。
方法签名 |
方法说明 |
|
InetAddress getInetAddress() |
返回套接字所连接的地址 |
|
InputStream getInputStream() |
返回此套接字的输入流 |
|
OutputStream getOutputStream() |
返回此套接字的输出流 |
长连接(long connnection),指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
短连接(short connnection),是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时才去建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送。
基于TCP套接字的回显程序执行顺序:结合代码观看
1.TcpEchoServer线启动,运行start(),阻塞状态
2.TcpEchoClient启动,会调用socket的构造方法,和服务器进行连接,连接成功之后,accept就会返回
3.对于服务器,进入processConnection方法,尝试从客户端读取请求,由于此时客户端没有发送请求,此时读取操作处于阻塞状态;对于客户端,从控制台读取用户输入。
4.当用户输入后,客户端就会真正发送请求,同时往下执行到,读取服务器响应,再次阻塞。
5.服务器收到客户端的请求之后,从next返回,执行process,执行println将响应写回给客户端。
6.服务器重新回到循环开头位置,继续尝试读取请求,并且阻塞;客户端收到服务器的响应,就可以把结果显示出来了,同时进行下次循环,等待用户输入。
(1)TcpEchoServer
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpEchoServer {
//serverSocket相当于外场的拉客中介,只能有一个
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
//使用线程池,使得一个连接的所有请求处理完,该线程不会直接被销毁,而是换到池子里面,下次连接时还能直接使用
ExecutorService executorService = Executors.newCachedThreadPool();
System.out.println("服务器启动");
while (true) {
//clientSocket相当于内场带每个用户讲解房子的中介,可以有多个
Socket clientSocket = serverSocket.accept();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
//通过processConnection此方法来处理一个连接
//读取请求
//根据请求计算响应
//将相应返回到客户端
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()){
//相当于字符流
//使用scanner的方式读取数据
Scanner scanner = new Scanner(inputStream);
//将outputStream转换为字符流
PrintWriter printWriter = new PrintWriter(outputStream);
while(true) {
//1.读取请求
//每个请求是个字符串(文本数据)
//请求与请求之间,使用\n来分割
//hasNext():判断接下来是否还有数据
//如果客户端关闭连接,hasNext就会返回为false,循环就会结束
if (!scanner.hasNext()) {
//读取到流结尾(也就是客户端关闭了)
//输出客户端ip地址,输出客户端端口号
System.out.printf("[%s:%d]客户端下线\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//如果客户端有数据,hasNext就会返回为true,进一步使用下面的next方法来读取出这一段字符串的内容
//直接使用scanner读取一段字符串
//next往后一直都,督导空白符结束(空格,换行,制表符,翻页符等等)
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端,next读操作结果是不带换行的,响应也需要带上换行
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可以有多个,而且生命周期端,所以需要关闭
clientSocket.close();
}
}
//根据请求计算响应
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
(2)TcpEchoClient
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serveIp,int port) throws IOException {
//将客户端于服务端建立TCP连接
//连接上了,服务器的accept就会返回,在此之前,accept是处于阻塞状态的
socket = new Socket(serveIp,port);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while (true) {
//1.把键盘上读取用户输入的内容
System.out.print("->");
String request = scanner.next();
//2.把读取的内容构造成请求,发送给服务器
//发送带有换行
//以下步骤只是把数据写入了内存的缓冲区中,等到缓冲区满了(没有满是因为数据不够多),才会真正写网卡
printWriter.println(request);
//使用flush()方法,手动刷新缓冲区,将数据立即写入网卡
printWriter.flush();
//3.从服务器读取相应内容
String response = scannerFromSocket.next();
//4.把响应结果显示到控制台上
System.out.printf("req:%s;resp:%s\n", request, response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);
tcpEchoClient.start();
}
}