@TOC
TCP 与 UDP 的 区别
有连接 与 无连接
可以怎么去理解?
比如说:现在我们要打电话给某个朋友。输入号码,按下手机拨号键。手机开始发出 嘟嘟嘟 声音,开始等待对方接听, 而且,我们拨号之后,并不是马上就能接通的!必须要等待 对方接听之后,我们才能与其交流。 之所以说:有链接 就像 打电话一样,是因为 打电话,必须要接通了之后,才能交流;没有接通,双方就无法交流。有连接的意思:就是在两者确认建立联系后,就可以开始交互了。
不需要接通,直接就能发数据。发微信,我们都知道:发送信息的时候,是不需要对方在线或者回复,按下回车,立马就能加个信息发送出去,不过 对方 看没看见这条消息,我们是不确定的 。这种情况,就叫做无连接。
注:
- TCP,就是要求双发先建立连接,连接好了,才能进行传数据。
- UDP,直接传输数据,不需要双方建立连接
面向字节流 与 数据报
这个就非常类似于 文件操作中的字节流。网络传输也是一样!假设,现有100个字节的数据。我们可以一直发完。也可以 一次发 10个字节,发送十次。也可以 一次发 2 个字节,发送50次。…
一个数据报都会明确大小。一次 发送/接收 必须是 一个 完整的数据报。不能是半个,也不能是一个半,必须是整数个。
在代码中,这两者的区别是非常明显的!
通信五元组
端口号(port)是传输层协议的内容.
端口号是一个32位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
源IP就好比发件人的地址 源端口就好比发件人的姓名
目的IP就好比收件人的地址 目的端口就好比收件人的姓名
UDP Socket编程
TCP 和 UDP 协议中,只有 UDP 是面向数据报的。那么 DatagramScoket 和 DatagramPacket 这两类,从名字就能看出来(Datagram-数据报),是关于UDP协议的类
DatagramSocket(数据报套接字)
UDP 主要接口
- DatagramSocket(int port,InetAddress laddr) 创建一个数据报套接字,绑定到指定的本地地址
- DatagramSocket(SocketAddress bindaddr) 创建一个数据报套接字,绑定到指定的本地套接字地址
- void bind(SocketAddress addr) 将此DatagramSocket绑定到特定的地址和端口
- void connect(InetAddress address, int port) 将套接字连接到此套接字的远程地址
- void receive(DatagramPacket p) 从此套接字接收数据报包
- void close() 关闭此数据报套接字
- void send(DatagramPacket p) 从此套接字发送数据报包
使用UDP实现汉译英 C/S
服务端
// 实现翻译回显C/S public class MyUDPServer { // 对于一个服务器程序来说, 核心流程也是要分成两步. // 1. 进行初始化操作 (实例化 Socket 对象) // 2. 进入主循环, 接受并处理请求. (主循环就是一个 "死循环") // a) 读取数据并解析 // b) 根据请求计算响应 // c) 把响应结果写回到客户端. private DatagramSocket socket =null; //map去存储我们的汉译英数据 private Map<String, String> map = new HashMap<>(); public MyUDPServer(int port) throws SocketException { //添加数据 initDates(); //服务器new socket对象的时候需要和一个ip地址和端口号绑定起来 //如果没有写ip 则默认时0.0.0.0 (一个特殊的ip会关联到这个主机的国有网卡的ip) //socket对象本省就是一个文件 socket = new DatagramSocket(port); } // map初始化 private void initDates() { map.put("猫", "cat"); map.put("猪", "pig"); map.put("狗", "dog"); map.put("人", "people"); map.put("笔", "pen"); map.put("坐", "sit"); map.put("手", "hand"); map.put("腿", "leg"); } //启动服务器 public void start() throws IOException { System.out.println("服务器启动"); // UDP 不用建立连接, 接受盛怒据即可 while(true){ //1. 接受客户端的请求 //2. 根据请求计算相应 //3. 把响应写回客户端 //这是一个接受数据的缓冲区 地址是接受数据的时候有内存填充 DatagramPacket datagramPacket= new DatagramPacket(new byte[4096],4096); //程序启动会很快到达receive操作 如果客户端没有发送任何数据 此时receive操作会阻塞直到有客户端发送数据过来 //1.当整的有哭换端发送过来数据时 receive就会将数据保证到DategramPAcket对象的缓冲区里 socket.receive(datagramPacket); //原本请求的数据时byte[]需要将其转换成String 并且如果发来的数据小于我们缓冲区的大小就会默认添加空格 我们得去掉无用空格 String request = new String(datagramPacket.getData(), 0, datagramPacket.getLength(),"UTF-8").trim(); //2.请求计算相应 String respond = process(request); //把响应写回给客户端, 响应数据就是 response, 需要包装成一个 DatagramPacket 对象 //此时这个用于send 不仅需要指定缓冲区还不要忘记在Packet对象的最后加上请求数据包里的Socket地址 //填写ip和port还可以自己手动设置将ip和port分开写(如下面案例) 还可以直接定义InetAddress对象(里面包含ip和port) DatagramPacket respondPacket = new DatagramPacket(respond.getBytes(), respond.getBytes().length, datagramPacket.getSocketAddress()); // 3。发送数据 socket.send(respondPacket); //打印请求访问日志 System.out.println(respondPacket.getAddress().toString() + " " + respondPacket.getPort() + " request: " + request + " respond: " + respond); } } private String process(String request) { return map.getOrDefault(request, "未学习"); } //一个主函数去设置该服务器的端口 并让其开始执行 public static void main(String[] args) { try { MyUDPServer myUDPServer = new MyUDPServer(9090); try { myUDPServer.start(); } catch (IOException e) { e.printStackTrace(); } } catch (SocketException e) { e.printStackTrace(); } } }客户端
//客户端程序 public class MyUDPClient { //核心操作有俩步 //启动客户端的时候需要指定连接那台服务器 //执行任务主要流程分4步 // 1. 从用户这里读取输入的数据. // 2. 构造请求发送给服务器 // 3. 从服务器读取响应 // 4. 把响应写回给客户端. //需要客户端知道要发往哪台服务器的ip 和端口 还需要一个udp的连接对象 private String severIP = "127.0.0.1"; private int severPort = 9090; private DatagramSocket socket = null; //需要在启动客户端的时候来指定需要连接哪个服务器 public MyUDPClient(String severIP, int severPort) throws SocketException { this.severIP = severIP; this.severPort = severPort; //客户端在创建socket的时候不需要绑定端口号 但是服务器必须绑定端口号 //因为服务器绑定了端口号 客户端才能找到去访问它 //客户端不绑定是为了可以在一台主机上启动多个客户端 this.socket = new DatagramSocket(); } public void start() throws IOException { Scanner scanner = new Scanner(System.in); while (true) { //读取用户输入的消息 System.out.print("输入字符串->"); String request = scanner.nextLine(); if ("exit".equals(request)) { break; } //发送请求 //注意ip和port要分开写并且前后位置要注意 DatagramPacket requstPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(this.severIP), this.severPort); socket.send(requstPacket); //接收服务器的响应 DatagramPacket respondPacket = new DatagramPacket(new byte[4096], 4096); socket.receive(respondPacket); String respond = new String(respondPacket.getData(), 0, respondPacket.getLength()).trim(); //显示响应 System.out.println(respond); } } public static void main(String[] args) { try { //此时我们用于自己主机实验 127.0.0.1是一个特殊的ip(环回ip) 自己访问自己 //如果服务器和客户端在同一台主机上旧使用环回ip 如果不在同一台主机上就必须填写服务器的ip //端口号必须与服务器的端口号一致 MyUDPClient client = new MyUDPClient("127.0.0.1", 9090); try { client.start(); } catch (IOException e) { e.printStackTrace(); } } catch (SocketException e) { e.printStackTrace(); } } }效果:
TCP socket编程
ServerSocket 与 Socket
ServerSocket类
- ServerSocket(int port) 创建绑定到指定端口的服务器套接字
- ServerSocket(int port, int backlog) 创建服务器套接字并将其绑定到指定的本地端口号,并指定了积压。
- Socket accept() 侦听要连接到此套接字并接受它
- bind(SocketAddress endpoint) 将ServerSocket绑定到特定地址(IP地址和端口号)
- InetAddress getInetAddress() 返回此服务器套接字的本地地址
- void close() 关闭此套接字
- int getLocalPort() 返回此套接字正在侦听的端口号
- 重要方法 accept()
Socket类
- Socket(InetAddress address, int port) 创建流套接字并将其连接到指定IP地址的指定端口号
- Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
- void bind(SocketAddress bindpoint) 将套接字绑定到本地地址
- void connect(SocketAddress endpoint) 将此套接字连接到服务器
- InetAddress getInetAddress() 返回套接字所连接的地址
- InputStream getInputStream() 返回此套接字的输入流
- OutputStream getOutputStream() 返回此套接字的输出流
简单的TCP服务器与客户端
服务端
public class TcpEchoServer { public ServerSocket serverSocket =null; public TcpEchoServer (int port) throws IOException { serverSocket =new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true) { // 由于 TCP 是有连接的,不能一上来就读数据, 要先建立连接 (接电话) // accept 就是在”接电话“,接电话的前提是,有人给你打~~, 如果没有客户端尝试建立连接, accept 就会阻塞 // accept 返回一个 socket 对象, 称为 clientSocket , 后续和客户端之间的沟通, 都是通过 clientSocket 完成的 Socket cilentSocket = serverSocket.accept(); processConnection(cilentSocket); } } private void processConnection(Socket cilentSocket) throws IOException { //打印客户端信息 System.out.printf("[%s:%d] 客户端建立连接!!\n",cilentSocket.getInetAddress().toString(),cilentSocket.getPort()); //处理请求和响应 全双工 try( InputStream inputStream = cilentSocket.getInputStream()) { try(OutputStream outputStream = cilentSocket.getOutputStream()){ //循环处理每个请求,返回响应 Scanner scanner =new Scanner(inputStream); while(true) { //读取请求 if(!scanner.hasNext()){ System.out.printf("[%s:%d] 客户端断开链接!!",cilentSocket.getInetAddress().toString(),cilentSocket.getPort()); break; } // 此处用Scanner 更方便 String request = scanner.next(); //根据请求计算响应 String response=process(request); //为了方便使用 用 PrintWrite 把 OutputStream 包裹 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); //刷新缓冲区,没有可能不能第一时间看见响应结果 printWriter.flush(); System.out.printf("[%s,%d] req:%s , resp:%s\n",cilentSocket.getInetAddress().toString(), cilentSocket.getPort(),request,response); } } } catch (IOException e) { e.printStackTrace(); } } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer tcpEchoServer = new TcpEchoServer(9090); tcpEchoServer.start(); } }客户端
public class TcpEchoClient { // 用普通的 Socket 即可,不用 ServerSocket 了 private Socket socket = null; //此处也不用手动给客户端指定端口号,由系统自动分配(隐式) public TcpEchoClient(String serverIP,int serverPort) throws IOException { // 其实这里是可以给定端口号的,但是这里给了之后,含义是不同的。 // 这里传入的 IP 与 端口号 的 含义: 表示的不是自己绑定,而是表示 和 这个IP 端口 建立连接 socket = new Socket(serverIP,serverPort);// 这里表示 与 IP 为serverIP的主机上的 端口为9090的程序,建立连接。 } public void start(){ System.out.println("和进服务器连接成功!"); Scanner sc = new Scanner(System.in); try(InputStream inputStream = socket.getInputStream()){ try (OutputStream outputStream = socket.getOutputStream()){ while(true){ //1、从控制台读取字符串 System.out.println("->"); String request = sc.next(); //2、根据读取的自妇产,构造请求,把请求发送给服务器 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(request);// 看似是一个输出语句,其实已经将数据写到服务器里面去了 printWriter.flush();// 记得 立即刷新缓冲区,确保 服务器 第一时间 感知到 请求。 //3、从服务器读取响应,并解析 Scanner scanner = new Scanner(inputStream); String response = scanner.next(); //4、把结果显示到控制台上。 System.out.printf("request:%s,response:%s\n ",request,response); } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } }效果
拓展:
多个客户端 与 服务器建立连接
虽然此时的 TCP代码已将跑起来了还是此处还存在一个很严重的问题!!!!
当前的服务器,同一时刻只能处理一个客户端连接。作为一个服务器应该给很多客户端提供服务,而这里只能处理一个客户端,这显然是不科学的。
程池版本——TCP服务器
- 观察运行上述代码我们发现再启动一个客户端, 尝试连接服务器, 发现第二个客户端, 不能正确的和服务器进行通信.分析原因, 是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求.我们当前的这个TCP, 只能处理一个连接, 这是不科学的
- 所以我们通过每个请求, 创建子进程的方式来支持多连接
- 但是还有问题当我们有很多连接的时候 线程就会疯狂的创建和销毁 所以结合前面所学我们可以使用线程池进行优化
-
线程池 优点利用线程池管理并复用线程、控制最大并发数等。实现任务线程队列缓存策略和拒绝机制。实现某些与时间相关的功能,如定时执行、周期执行等。隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;
因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免个服务线程互相影响。
- 线程池 缺点前期需要创建多个线程示例对象。如果客户端连接少,会造成线程资源浪费=