网络编程(详细)-程序员宅基地

技术标签: Java  网络  服务器  网络协议  

网络编程的概念

地球村

随着广播、电视、互联网的出现,随着各种现代交通方式的飞速发展,人与人之间的时空距离骤然缩短,整个世界紧缩成一个“村落”。

地球村也译为世界村(global village),对地球的一种比喻说法。现代科技的迅速发展,缩小了地球上的时空距离,国际交往日益频繁便利,因而整个地球就如同是茫茫宇宙中的一个小村落。

计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统网络管理软件网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

计算机网络主要是由一些通用的、可编程的硬件互连而成的,而这些硬件并非专门用来实现某一特定目的(例如,传送数据或视频信号)。这些可编程的硬件能够用来传送多种不同类型的数据,并能支持广泛的和日益增长的应用。

数据通信

是计算机网络的最主要的功能之一。数据通信是依照一定的通信协议,利用数据传输技术在两个终端之间传递数据信息的一种通信方式和通信业务。它可实现计算机和计算机、计算机和终端以及终端与终端之间的数据信息传递,是继电报、电话业务之后的第三种最大的通信业务。数据通信中传递的信息均以二进制数据形式来表现,数据通信的另一个特点是总是与远程信息处理相联系,是包括科学计算、过程控制、信息检索等内容的广义的信息处理。

网络编程从大的方面说就是对信息的发送到接收,中间传输为物理线路的作用。

网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理,代码、开发工具、数据库、服务器架设和网页设计这5部分你都要接触。

网络模型

  • https://blog.csdn.net/troubleshooter/article/details/122376824

网络是一个复杂的系统,不仅包括大量的应用程序端系统通信链路、分组交换机等,还有各种各样的协议组成。

为了给网络协议的设计提供一个结构,网络设计者以分层(layer)的方式组织协议,每个协议属于层次模型之一。每一层都是向它的上一层提供服务(service),即所谓的服务模型(service model)。每个分层中所有的协议称为协议栈(protocol stack)。

OSI 七层模型

OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。

OSI 定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。

每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。

七层模型释义
OSI 参考模型 功能定义 设备 协议 数据单元
应用层 为应用程序提供服务 应用程序 HTTP,HTTPS,FTP,POP3、SMTP、Telnet、SSH、DHCP、DNS 数据(Data)
表示层 数据格式转换和加密
会话层 建立、管理和维护会话
传输层 建立、管理和维护端到端的连接 进程、端口 TCP、UDP 数据段(Segment)
网络层 IP地址及路由选择 路由器、交换机、防火墙 IP、ARP、ICMP、IGMP 数据包(Packet)
数据链路层 提供介质访问和链路管理 网桥、以太网交换机、网卡 ARP、RARP、IEEE802.3、PPP、CSMA/CD 数据帧(Frame)
物理层 传输介质 中继器、集线器、双绞线 FE自协商 Manchester MLT-3 4A PAM5 数据位(bit)

应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

实际公司A的老板就是我们所述的用户,而他要发送的商业报价单,就是应用层提供的一种网络服务,当然,老板也可以选择其他服务,比如说,发一份商业合同,发一份询价单,等等。

表示层

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

由于公司A和公司B是不同国家的公司,他们之间的商定统一用英语作为交流的语言,所以此时表示层(公司的文秘),就是将应用层的传递信息转翻译成英语。同时为了防止别的公司看到,公司A的人也会对这份报价单做一些加密的处理。这就是表示的作用,将应用层的数据转换翻译等。

会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

会话层的同事拿到表示层的同事转换后资料,(会话层的同事类似公司的外联部),会话层的同事那里可能会掌握本公司与其他好多公司的联系方式,这里公司就是实际传递过程中的实体。他们要管理本公司与外界好多公司的联系会话。当接收到表示层的数据后,会话层将会建立并记录本次会话,他首先要找到公司B的地址信息,然后将整份资料放进信封,并写上地址和联系方式。准备将资料寄出。等到确定公司B接收到此份报价单后,此次会话就算结束了,外联部的同事就会终止此次会话。

传输层

传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

传输层就相当于公司中的负责快递邮件收发的人,公司自己的投递员,他们负责将上一层的要寄出的资料投递到快递公司或邮局。

网络层

本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。

网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说,从深圳发往北京的顺丰快递(陆运为例啊,空运好像直接就飞到北京了),首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。

数据链路层

将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。

数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。

MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。

物理层

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

快递寄送过程中的交通工具,就相当于我们的物理层,例如汽车,火车,飞机,船。

通信特点

对等通信:为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。

TCP/IP 五层模型

TCP/IP五层协议和OSI的七层协议对应关系如下:

OSI 参考模型 TCP/IP 五层模型
应用层 应用层
表示层
会话层
传输层 传输层
网络层 网络层
数据链路层 数据链路层
物理层 物理层

区别

  • 前者是七层模型,后者是五层结构;
  • OSI是一个完整的、完善的宏观理论模型;而TCP/IP(参考)模型,更加侧重的是互联网通信核心(也是就是围绕TCP/IP协议展开的一系列通信协议)的分层;
  • 对可靠性要求不同(后者更高);
  • 实际应用场景不同(OSI模型只是理论上的模型,并没有成熟的产品,而TCP/IP已经成为“实际上的国际标准)。

网络编程的目的

直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。

网络编程中的两个要素

  • 通信双方的地址:IP 和 端口号
  • 提供网络通信协议:TCP/IP参考模型(应用层、传输层、网络层、物理+数据链路层)

IP 地址

IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。

IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP协议中还有一个非常重要的内容,那就是给因特网上的每台计算机和其它设备都规定了一个唯一的地址,叫做“IP地址”。由于有这种唯一的地址,才保证了用户在连网的计算机上操作时,能够高效而且方便地从千千万万台计算机中选出自己所需的对象来。

IP 地址类型

  • https://baike.baidu.com/item/IP%E5%9C%B0%E5%9D%80

公有地址

公有地址(Public address)由Inter NIC(Internet Network Information Center因特网信息中心)负责。这些IP地址分配给注册并向Inter NIC提出申请的组织机构。通过它直接访问因特网

私有地址

私有地址(Private address)属于非注册地址,专门为组织机构内部使用。

以下列出留用的内部私有地址:

类别 最大网络数 IP地址范围 单个网段最大主机数 私有IP地址范围
A 126(2^7-2) 1.0.0.1-127.255.255.254 16777214 10.0.0.0-10.255.255.255
B 16384(2^14) 128.0.0.1-191.255.255.254 65534 172.16.0.0-172.31.255.255
C 2097152(2^21) 192.0.0.1-223.255.255.254 254 192.168.0.0-192.168.255.255

IPv4

网际协议版本4(英语:Internet Protocol version 4IPv4),又称互联网通信协议第四版,是网际协议开发过程中的第四个修订版本,也是此协议第一个被广泛部署的版本。IPv4是互联网的核心,也是使用最广泛的网际协议版本,其后继版本为IPv6,直到2011年,IANA IPv4位址完全用尽时,IPv6仍处在部署的初期。

环回地址:127.0.0.1,指本地机,一般用来测试使用。对于大多数习惯用localhost的来说,实质上就是指向127.0.0.1这个本地IP地址。在操作系统中有个配置文件(windows中路径为 C:\WINDOWS\system32\drivers\etc\hosts,Unix/Linux路径为 /etc/hosts )将localhost与127.0.0.1绑定在了一起。

IPv6

IPv6是英文“Internet Protocol Version 6”(互联网协议第6版)的缩写,是互联网工程任务组(IETF)设计的用于替代IPv4的下一代IP协议,其地址数量号称可以为全世界的每一粒沙子编上一个地址。

IPv6的地址长度为128位(8个无符号整数),是IPv4地址长度的4倍。

IP 地址 Java Object

/**
 * ip地址
 */
@Test
public void inetAddressTest() throws UnknownHostException {
    
    System.out.println(InetAddress.getLocalHost());// PC-20220209TGKW/192.168.254.1
    System.out.println(InetAddress.getLocalHost().getHostName());// PC-20220209TGKW
    System.out.println(InetAddress.getLocalHost().getHostAddress());// 192.168.254.1
    System.out.println(InetAddress.getLocalHost().getCanonicalHostName());// PC-20220209TGKW
    System.out.println();

    System.out.println(InetAddress.getByName("127.0.0.1"));// /127.0.0.1
    System.out.println(InetAddress.getByName("127.0.0.1").getHostName());// 127.0.0.1
    System.out.println(InetAddress.getByName("127.0.0.1").getHostAddress());// 127.0.0.1
    System.out.println(InetAddress.getByName("127.0.0.1").getCanonicalHostName());// 127.0.0.1
    System.out.println();

    System.out.println(InetAddress.getByName("localhost"));// localhost/127.0.0.1
    System.out.println(InetAddress.getByName("localhost").getHostName());// localhost
    System.out.println(InetAddress.getByName("localhost").getHostAddress());// 127.0.0.1
    System.out.println(InetAddress.getByName("localhost").getCanonicalHostName());// 127.0.0.1
    System.out.println();

    System.out.println(InetAddress.getByName("www.baidu.com"));// www.baidu.com/39.156.66.14
    System.out.println(InetAddress.getByName("www.baidu.com").getHostName());// www.baidu.com
    System.out.println(InetAddress.getByName("www.baidu.com").getHostAddress());// 39.156.66.14
    System.out.println(InetAddress.getByName("www.baidu.com").getCanonicalHostName());// 39.156.66.14
}

端口号

所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。

网络技术中,端口包括逻辑端口和物理端口两种类型。物理端口是用于连接物理设备之间的接口,如ADSL Modem、集线器交换机路由器上用于连接其他网络设备的接口,如RJ-45端口、SC端口等等。逻辑端口是指逻辑意义上用于区分服务的端口,比如用于浏览网页服务的80端口,用于FTP服务的21端口等。如TCP/IP协议中的服务端口,通过不同的逻辑端口来区分不同的服务。一个IP地址的端口通过16bit进行编号,最多可以有65536个端口 。端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535。

端口号分类

  • 公认端口

    从0到1023,它们紧密绑定(binding)于一些服务,已被占用。通常这些端口的通讯明确表明了某种服务的协议。例如:80端口实际上总是HTTP通讯。

  • 注册端口

    从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。

  • 动态和/或私有端口

    从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。

使用规则

TCP与UDP段结构中端口地址都是16比特,可以有在 0---65535 范围内的端口号。对于这65536个端口号有以下的使用规定:

  1. 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1—1023之间的端口号,是由ICANN来管理的;端口号从1024—49151是被注册的端口,也成为“用户端口”,被IANA指定为特殊服务使用;
  2. 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号;
  3. 大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。

windows 端口命令

# 查看所有端口
netstat -ano

# 查看指定端口,示例:4092
netstat -ano | findstr "4092"

# 查看指定端口的进程,示例:微信
tasklist | findstr "4092"

IP 套接字 Java Object

套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。

/**
 * IP地址:端口号
 */
@Test
public void inetSocketAddressTest() {
    
    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 4092);
    InetSocketAddress inetSocketAddress2 = new InetSocketAddress("localhost", 4092);
    System.out.println(inetSocketAddress);///127.0.0.1:4092
    System.out.println(inetSocketAddress2);//localhost/127.0.0.1:4092
    System.out.println();
    System.out.println(inetSocketAddress.getAddress());///127.0.0.1
    //获取ip地址
    System.out.println(inetSocketAddress.getHostName());//127.0.0.1
    //获取ip地址(字符串)
    System.out.println(inetSocketAddress.getHostString());//127.0.0.1
    //获取端口
    System.out.println(inetSocketAddress.getPort());//127.0.0.1
}

通信协议

协议:在传输数据的过程中需要遵守的规则。

开放系统互联协议中最早的协议之一,它为连接不同操作系统和不同硬件体系结构互联网络提供通信支持,是一种网络通用语言。TCP/IP协议定义了在互联网络中如何传递、管理信息(文件传送、收发电子邮件、远程登录等),并制定了在出错时必须遵循的规则。

通讯协议又称通信规程,是指通信双方对数据传送控制的一种约定。约定中包括对数据格式,同步方式,传送速度,传送步骤,检纠错方式以及控制字符定义等问题做出统一规定,通信双方必须共同遵守,它也叫做链路控制规程。

TCP/IP 协议簇

TCP/IP是网络中使用的基本的通信协议。虽然从名字上看TCP/IP包括两个协议,传输控制协议(TCP)和网际协议(IP),但TCP/IP实际上是一组协议,它包括上百个各种功能的协议,如:远程登录、文件传输和电子邮件等,而TCP协议和IP协议是保证数据完整传输的两个基本的重要协议。通常说TCP/IP是Internet协议族,而不单单是TCP和IP。我们通常称它为TCP/IP协议族。

之所以说TCP/IP是一个协议族,是因为TCP/IP协议包括 TCP、IP、UDP、ICMP、RIP、TELNETFTP、SMTP、ARP、TFTP等许多协议,这些协议一起称为TCP/IP协议。

  • TCP:传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
  • IP:网际互连协议,Internet Protocol的缩写,是TCP/IP体系中的网络层协议。
  • UDP:用户数据报协议,为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。

Internet 的传输层有两个主要协议,互为补充。无连接的是 UDP,它除了给应用程序发送数据包功能并允许它们在所需的层次上架构自己的协议之外,几乎没有做什么特别的事情。面向连接的是 TCP,该协议几乎做了所有的事情。

TCP 同 UDP 对比

当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。

TCP UDP
是否连接 面向连接 无连接
是否可靠 可靠连接,使用流量控制和拥塞控制 不可靠传输,不使用流量控制和拥塞控制
连接对象个数 只能是一对一通信 支持一对一,一对多,多对多交互通信
传输方式 面向字节流 面向报文
首部开销 首部最小20个字节,最大60个字节 首部开销小,仅8个字节
适用场景 适用于要求可靠传输的应用,例如文件传输 适用于实时应用(IP电话、视频会议、直播等)

三次握手(TCP连接过程)

  1. 第一次握手:客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

    客户端---->服务端:客户端向服务端请求连接

  2. 第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

    客户端<----服务端:服务端告诉客户端可以连接

  3. 第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

    客户端---->服务端:客户端直接连接服务端

为什么 TCP 建立连接需要三次握手,而不是两次?

这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

四次挥手(TCP断开链接)

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

  1. 第一次挥手:若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
  2. 第二次挥手:B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。
  3. 第三次挥手:B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
  4. 第四次挥手:A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

TCP

实现聊天

服务端

  1. 建立服务端口 ServerSocket
  2. 等待客户端连接 accept
  3. 接口客户端消息
  4. 关闭客户端连接
  5. 等待下一个客户端的连接…
/**
 * 数据传输服务端
 */
@Test
public void serverTest() {
    
    ServerSocket serverSocket = null;
    try {
    
        serverSocket = new ServerSocket(8080);
        System.out.println("##########服务端启动完成!");
        Socket socket = null;
        InputStream is = null;
        OutputStream os = null;
        while (true) {
    
            try {
    
                System.out.println("##########等待连接~~~");
                socket = serverSocket.accept();
                System.out.println(String.format("##########已接收到客户端的连接,IP:%s,端口:%d", socket.getInetAddress().getHostName(), socket.getPort()));
                is = socket.getInputStream();

                //接收客户端消息
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int len;
                byte[] buffer = new byte[1024];
                if ((len = is.read(buffer)) != -1) {
    
                    baos.write(buffer, 0, len);
                }
                System.out.println("接收到数据: " + baos.toString());

                //响应客户端消息
                os = socket.getOutputStream();
                os.write("叽里呱啦哒哒哒~~~".getBytes());
            } catch (IOException e) {
    
                e.printStackTrace();
            } finally {
    
                try {
    
                    if (os != null) {
    
                        os.close();
                    }
                } catch (IOException e) {
    
                    e.printStackTrace();
                }
                try {
    
                    if (is != null) {
    
                        is.close();
                    }
                } catch (IOException e) {
    
                    e.printStackTrace();
                }
                try {
    
                    if (socket != null) {
    
                        socket.close();
                        System.out.println("##########已关闭客户端连接");
                    }
                } catch (IOException e) {
    
                    e.printStackTrace();
                }
            }
        }
    } catch (IOException e) {
    
        e.printStackTrace();
    } finally {
    
        try {
    
            if (serverSocket != null) {
    
                serverSocket.close();
            }
        } catch (IOException e) {
    
            e.printStackTrace();
        }

    }
}

客户端

  1. 连接服务器 Socket
  2. 向服务器发送消息
  3. 关闭连接
/**
 * 数据传输客户端
 */
@Test
public void clientTest() {
    
    Socket socket = null;
    OutputStream os = null;
    InputStream is = null;
    try {
    
        //连接服务端
        socket = new Socket("127.0.0.1", 8080);
        //向服务端发送消息
        os = socket.getOutputStream();
        os.write("你好,我是客户端,来聊五毛钱的~~".getBytes());
        //接收服务端消息
        is = socket.getInputStream();

        /*
        StringBuilder sb = new StringBuilder();
        int len;
        byte[] bytes = new byte[1024];
        if ((len = is.read(bytes)) != -1) {
            sb.append(new String(bytes, 0, len));
        }
        System.out.println("客户端接收到数据: " + sb.toString());
        */

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int len;
        byte[] buffer = new byte[1024];
        if ((len = is.read(buffer)) != -1) {
    
            baos.write(buffer, 0, len);
        }

        System.out.println(baos.toString());
    } catch (IOException e) {
    
        e.printStackTrace();
    } finally {
    
        try {
    
            if (is != null) {
    
                is.close();
            }
        } catch (IOException e) {
    
            e.printStackTrace();
        }
        try {
    
            if (os != null) {
    
                os.close();
            }
        } catch (IOException e) {
    
            e.printStackTrace();
        }
        try {
    
            if (socket != null) {
    
                socket.close();
            }
        } catch (IOException e) {
    
            e.printStackTrace();
        }
    }
}

实现文件上传

服务端

/**
 * 文件上传服务端
 */
@Test
public void uploadServerTest() throws IOException {
    
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
    
        System.out.println("等待客户端连接......");
        Socket socket = serverSocket.accept();
        System.out.println("接收到文件~~~");
        FileOutputStream fos = new FileOutputStream(new File("D:\\" + System.currentTimeMillis() + ".png"));
        InputStream is = socket.getInputStream();
        int len;
        byte[] buffer = new byte[1024];
        while ((len = is.read(buffer)) != -1) {
    
            System.out.println(StringUtils.getUUID());
            fos.write(buffer, 0, len);
        }

        System.out.println("----------------------------------");

        OutputStream ops = socket.getOutputStream();
        ops.write("服务端接收成功~~~".getBytes());

        ops.close();
        is.close();
        fos.close();
        socket.close();

        System.out.println("接收文件成功~~~");
    }
}

客户端

/**
 * 文件上传客户端
 */
@Test
public void uploadClientTest() throws IOException {
    
    Socket socket = new Socket("localhost", 8080);
    OutputStream ops = socket.getOutputStream();
    FileInputStream fis = new FileInputStream("C:\\uploadFileTest.png");
    int len;
    byte[] buffer = new byte[1024];
    while ((len = fis.read(buffer)) != -1) {
    
        ops.write(buffer, 0, len);
    }
    
    //通知服务器,文件发送完毕
    socket.shutdownOutput();

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    InputStream is = socket.getInputStream();
    while ((len = is.read(buffer)) != -1) {
    
        baos.write(buffer, 0, len);
    }

    System.out.println(baos.toString());

    is.close();
    fis.close();
    ops.close();
    socket.close();
}

客户端,如果不加 socket.shutdownOutput(); 就会阻塞。因为服务器会一直等待客户端的输出。既然服务器阻塞了,客户端等待着服务器的输出,也会被阻塞,所以导致客户端和服务端都被阻塞。

调用Socket.shutdownOutput()方法后,客户端输出的数据都将被发送,并加上 TCP 的正常连接终止序列(-1,也就是服务端终止循环的判断条件),这样服务端读取数据时就不会被阻塞了。

想必socket.close()也做了类似的事情。

但是本代码需要接收服务器的响应,所以不能关闭socket,只能单向关闭客户端输出流。

UDP

实现消息发送

接收端

/**
 * UDP实现消息发送(接收端)
 */
@Test
public void udpReceiverTest() throws IOException {
    
    //开放端口
    DatagramSocket socket = new DatagramSocket(8080);

    while (true) {
    
        byte[] buffer = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);

        //阻塞接收
        socket.receive(packet);

        //System.out.println(JSON.toJSONString(packet));
        String msg = new String(packet.getData(), 0, packet.getLength());
        System.out.println(msg);
        if ("88".equals(msg)) {
    
            break;
        }
    }

    socket.close();
}

发送端

  • 可单独运行并发送数据,不会报错
/**
 * UDP实现消息发送(发送端)
 */
@Test
public void udpSenderTest() throws IOException {
    
    //建立一个 Socket
    DatagramSocket socket = new DatagramSocket();
    //准备数据:控制台输入
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    while (true) {
    
        //创建数据包
        String msg = br.readLine();
        byte[] bytes = msg.getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getByName("127.0.0.1"), 8080);
        //发送数据包
        socket.send(packet);

        if ("88".equals(msg)) {
    
            break;
        }
    }

    br.close();
    //关闭连接
    socket.close();
}

多线程实现聊天

  1. 创建发送端线程类;
  2. 创建接收端线程类;
  3. 创建单元测试方法,模仿用户,分别开启发送线程和接收线程;
  4. 分别输入消息内容,回车发送;
  5. 查看对方窗口接收打印结果。

发送方线程类

public class Sender implements Runnable {
    

    DatagramSocket socket;
    BufferedReader br;
    String toIp;
    int toPort;

    public Sender(int fromPort, String toIp, int toPort) {
    
        this.toIp = toIp;
        this.toPort = toPort;
        try {
    
            socket = new DatagramSocket(fromPort);
        } catch (SocketException e) {
    
            e.printStackTrace();
        }
        br = new BufferedReader(new InputStreamReader(System.in));
    }

    @Override
    public void run() {
    

        while (true) {
    
            String msg = null;
            try {
    
                //创建数据包
                msg = br.readLine();
                byte[] bytes = msg.getBytes();
                DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length, InetAddress.getByName(toIp), toPort);
                //发送数据包
                socket.send(packet);
            } catch (IOException e) {
    
                e.printStackTrace();
            }

            if ("88".equals(msg)) {
    
                break;
            }
        }

        try {
    
            br.close();
        } catch (IOException e) {
    
            e.printStackTrace();
        }
        //关闭连接
        socket.close();
    }
}

接收方线程类

public class Receiver implements Runnable {
    

    private DatagramSocket socket;

    private final String fromName;

    public Receiver(int localPort, String fromName) {
    
        this.fromName = fromName;
        try {
    
            //开放端口
            socket = new DatagramSocket(localPort);
        } catch (SocketException e) {
    
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
    

        while (true) {
    
            try {
    
                byte[] buffer = new byte[1024];
                DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);

                //阻塞接收
                socket.receive(packet);

                //System.out.println(JSON.toJSONString(packet));
                String msg = new String(packet.getData(), 0, packet.getLength());
                System.out.println(fromName + ":" + msg);

                if ("88".equals(msg)) {
    
                    break;
                }
            } catch (IOException e) {
    
                e.printStackTrace();
            }
        }

        socket.close();
    }
}

单元测试类

/**
 * @author caoguangcong
 * @date 2022/10/8 23:20
 */
public class UdpChatTest {
    

    @Test
    public void talkStudentTest() {
    
        new Thread(new Receiver(8888, "李老师")).start();
        new Thread(new Sender(8881, "127.0.0.1", 7777)).start();

        while (true) {
    
            //防止jvm停止
        }

    }

    @Test
    public void talkTeacherTest() {
    
        new Thread(new Receiver(7777, "张三(学生)")).start();
        new Thread(new Sender(7771, "127.0.0.1", 8888)).start();

        while (true) {
    
            //防止jvm停止
        }

    }

}

URL

基本构成

@Test
public void urlTest() throws IOException {
    
    URL url = new URL("http://localhost:8080/projectName/index.jsp?username=zhangsan&password=123123");
    //协议
    System.out.println(url.getProtocol());
    //主机ip
    System.out.println(url.getHost());
    //端口号
    System.out.println(url.getPort());
    //协议默认端口号
    System.out.println(url.getDefaultPort());
    //文件路径
    System.out.println(url.getPath());
    //文件全路径
    System.out.println(url.getFile());
    //参数
    System.out.println(url.getQuery());
}
http
localhost
8080
80
/projectName/index.jsp
/projectName/index.jsp?username=zhangsan&password=123123
username=zhangsan&password=123123

下载资源

  1. tomcat 服务器webapps目录下创建文件:aladdin
  2. aladdin 文件夹下新建一个 txt 格式文件:demo.txt,内容随便书写;
  3. 双击 apache-tomcat-9.0.29\bin\startup.bat,启动 tomcat
  4. 执行 demo
  5. 查看项目根目录中的文件:file.txt,下载成功,内容与 demo.txt 内容一致。
@Test
public void url2Test() throws IOException {
    
    URL url = new URL("http://localhost:8080/aladdin/demo.txt");

    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    InputStream is = connection.getInputStream();

    FileOutputStream fos = new FileOutputStream("file.txt");

    int len;
    byte[] buffer = new byte[1024];
    while ((len = is.read(buffer)) != -1) {
    
        fos.write(buffer, 0, len);
    }

    fos.close();
    is.close();
    connection.disconnect();
}

BS架构服务器案例

BS结构服务器代码实现

/*
  问题1:
      我们获取的InputStream,可以吧请求信息利用read方法一个一节一个字节的读取出来
      但是众多的请求信息中,我们只要第一行的  GET /day19/web/index.html HTTP/1.1
      又但是,InputStream中没有一次读取一行的方法,BufferedReader中有readLine方法
      所以,我们就想办法能不能把InputStream转成BufferedReader?


   解决:
     BufferedReader(Reader in)

   问题2:
      a.我们创建BufferedReader的时候,构造方法的参数,是一个Reader(字符流),但是我们想要将字节流转成BufferedReader
        参数不匹配,不匹配就不能转,所以我们现在要考虑的问题是如何将
        ->字节流转成字符流(换句话说,只要能将InputStream放到BufferedReader的构造参数上就哦了)

   解决问题2:
       a. BufferedReader(Reader in)->Reader是一个 抽象类->FileReader
       b. 由于Reader是一个父类,所以其实我们BufferedReader的参数位置随意传,只要是Reader的子类就行
       c. InputStreamReader是Reader的子类,所以可以传递到BufferedReader构造参数位置
       d. InputStreamReader类的构造方法InputStreamReader(InputStream in)
       e.总结:我们可以利用转换流将InputStream转成BufferedReader对象
             new BufferedReader(new InputStreamReader(is))



   BufferedReader
      特有的方法:
         String readLine()
             构造:
                BufferedReader(Reader in)
 */
public class Server {
    
    public static void main(String[] args)throws Exception {
    
        //1.创建ServerSocket对象,指定端口号
        ServerSocket serverSocket = new ServerSocket(8888);

        while(true){
    
            //2.获取客户端对象
            Socket socket = serverSocket.accept();

            //3.调用getInputStream获取浏览器发过来的请求
            InputStream is = socket.getInputStream();

            //4.创建BufferedReader对象
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String s = br.readLine();//   GET /day14/web/index.html HTTP/1.1

            //5.处理字符串想办法得到day14/web/index.html
            String[] split = s.split(" ");
            String s1 = split[1];//    /day14/web/index.html
            String path = s1.substring(1);//   day14/web/index.html

            //6.创建FileInputStream读取本地的页面
            FileInputStream fis = new FileInputStream(path);
            //7.调用getOutputStream,往客户端响应页面
            OutputStream os = socket.getOutputStream();
            os.write("HTTP/1.1 200 OK\r\n".getBytes());//响应行
            os.write("Content-Type:text/html\r\n".getBytes());//响应头
            os.write("\r\n".getBytes());
            byte[] bytes = new byte[1024];
            int len;
            while((len = fis.read(bytes))!=-1){
    
                os.write(bytes,0,len);
            }

            //关流
            os.close();
            fis.close();
            br.close();
            is.close();
            socket.close();
        }
    }
}

BS结构服务器代码实现(多线程版本)

public class Server_Thread {
    
    public static void main(String[] args)throws Exception {
    
        //1.创建ServerSocket对象,指定端口号
        ServerSocket serverSocket = new ServerSocket(8888);

        while(true){
    
            //2.获取客户端对象
            Socket socket = serverSocket.accept();

            new Thread(()->{
    
                try{
    
                    //3.调用getInputStream获取浏览器发过来的请求
                    InputStream is = socket.getInputStream();

                    //4.创建BufferedReader对象
                    BufferedReader br = new BufferedReader(new InputStreamReader(is));
                    String s = br.readLine();//   GET /day14/web/index.html HTTP/1.1

                    //5.处理字符串想办法得到day14/web/index.html
                    String[] split = s.split(" ");
                    String s1 = split[1];//    /day14/web/index.html
                    String path = s1.substring(1);//   day14/web/index.html

                    //6.创建FileInputStream读取本地的页面
                    FileInputStream fis = new FileInputStream(path);
                    //7.调用getOutputStream,往客户端响应页面
                    OutputStream os = socket.getOutputStream();
                    os.write("HTTP/1.1 200 OK\r\n".getBytes());
                    os.write("Content-Type:text/html\r\n".getBytes());
                    os.write("\r\n".getBytes());
                    byte[] bytes = new byte[1024];
                    int len;
                    while((len = fis.read(bytes))!=-1){
    
                        os.write(bytes,0,len);
                    }

                    //关流
                    os.close();
                    fis.close();
                    br.close();
                    is.close();
                    socket.close();
                }catch (Exception e){
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_52610802/article/details/127233902

智能推荐

【史上最易懂】马尔科夫链-蒙特卡洛方法:基于马尔科夫链的采样方法,从概率分布中随机抽取样本,从而得到分布的近似_马尔科夫链期望怎么求-程序员宅基地

文章浏览阅读1.3k次,点赞40次,收藏19次。虽然你不能直接计算每个房间的人数,但通过马尔科夫链的蒙特卡洛方法,你可以从任意状态(房间)开始采样,并最终收敛到目标分布(人数分布)。然后,根据一个规则(假设转移概率是基于房间的人数,人数较多的房间具有较高的转移概率),你随机选择一个相邻的房间作为下一个状态。比如在巨大城堡,里面有很多房间,找到每个房间里的人数分布情况(每个房间被访问的次数),但是你不能一次进入所有的房间并计数。但是,当你重复这个过程很多次时,你会发现你更有可能停留在人数更多的房间,而在人数较少的房间停留的次数较少。_马尔科夫链期望怎么求

linux以root登陆命令,su命令和sudo命令,以及限制root用户登录-程序员宅基地

文章浏览阅读3.9k次。一、su命令su命令用于切换当前用户身份到其他用户身份,变更时须输入所要变更的用户帐号与密码。命令su的格式为:su [-] username1、后面可以跟 ‘-‘ 也可以不跟,普通用户su不加username时就是切换到root用户,当然root用户同样可以su到普通用户。 ‘-‘ 这个字符的作用是,加上后会初始化当前用户的各种环境变量。下面看下加‘-’和不加‘-’的区别:root用户切换到普通..._限制su root登陆

精通VC与Matlab联合编程(六)_精通vc和matlab联合编程 六-程序员宅基地

文章浏览阅读1.2k次。精通VC与Matlab联合编程(六)作者:邓科下载源代码浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程浅析VC与MATLAB联合编程  Matlab C/C++函数库是Matlab扩展功能重要的组成部分,包含了大量的用C/C++语言重新编写的Matlab函数,主要包括初等数学函数、线形代数函数、矩阵操作函数、数值计算函数_精通vc和matlab联合编程 六

Asp.Net MVC2中扩展ModelMetadata的DescriptionAttribute。-程序员宅基地

文章浏览阅读128次。在MVC2中默认并没有实现DescriptionAttribute(虽然可以找到这个属性,通过阅读MVC源码,发现并没有实现方法),这很不方便,特别是我们使用EditorForModel的时候,我们需要对字段进行简要的介绍,下面来扩展这个属性。新建类 DescriptionMetadataProvider然后重写DataAnnotationsModelMetadataPro..._asp.net mvc 模型description

领域模型架构 eShopOnWeb项目分析 上-程序员宅基地

文章浏览阅读1.3k次。一.概述  本篇继续探讨web应用架构,讲基于DDD风格下最初的领域模型架构,不同于DDD风格下CQRS架构,二者架构主要区别是领域层的变化。 架构的演变是从领域模型到C..._eshoponweb

Springboot中使用kafka_springboot kafka-程序员宅基地

文章浏览阅读2.6w次,点赞23次,收藏85次。首先说明,本人之前没用过zookeeper、kafka等,尚硅谷十几个小时的教程实在没有耐心看,现在我也不知道分区、副本之类的概念。用kafka只是听说他比RabbitMQ快,我也是昨天晚上刚使用,下文中若有讲错的地方或者我的理解与它的本质有偏差的地方请包涵。此文背景的环境是windows,linux流程也差不多。 官网下载kafka,选择Binary downloads Apache Kafka 解压在D盘下或者什么地方,注意不要放在桌面等绝对路径太长的地方 打开conf_springboot kafka

随便推点

VS2008+水晶报表 发布后可能无法打印的解决办法_水晶报表 不能打印-程序员宅基地

文章浏览阅读1k次。编好水晶报表代码,用的是ActiveX模式,在本机运行,第一次运行提示安装ActiveX控件,安装后,一切正常,能正常打印,但发布到网站那边运行,可能是一闪而过,连提示安装ActiveX控件也没有,甚至相关的功能图标都不能正常显示,再点"打印图标"也是没反应解决方法是: 1.先下载"PrintControl.cab" http://support.businessobjects.c_水晶报表 不能打印

一. UC/OS-Ⅱ简介_ucos-程序员宅基地

文章浏览阅读1.3k次。绝大部分UC/OS-II的源码是用移植性很强的ANSI C写的。也就是说某产品可以只使用很少几个UC/OS-II调用,而另一个产品则使用了几乎所有UC/OS-II的功能,这样可以减少产品中的UC/OS-II所需的存储器空间(RAM和ROM)。UC/OS-II是为嵌入式应用而设计的,这就意味着,只要用户有固化手段(C编译、连接、下载和固化), UC/OS-II可以嵌入到用户的产品中成为产品的一部分。1998年uC/OS-II,目前的版本uC/OS -II V2.61,2.72。1.UC/OS-Ⅱ简介。_ucos

python自动化运维要学什么,python自动化运维项目_运维学python该学些什么-程序员宅基地

文章浏览阅读614次,点赞22次,收藏11次。大家好,本文将围绕python自动化运维需要掌握的技能展开说明,python自动化运维从入门到精通是一个很多人都想弄明白的事情,想搞清楚python自动化运维快速入门 pdf需要先了解以下几个事情。这篇文章主要介绍了一个有趣的事情,具有一定借鉴价值,需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获,下面让小编带着大家一起了解一下。_运维学python该学些什么

解决IISASP调用XmlHTTP出现msxml3.dll (0x80070005) 拒绝访问的错误-程序员宅基地

文章浏览阅读524次。2019独角兽企业重金招聘Python工程师标准>>> ..._hotfix for msxml 4.0 service pack 2 - kb832414

python和易语言的脚本哪门更实用?_易语言还是python适合辅助-程序员宅基地

文章浏览阅读546次。python和易语言的脚本哪门更实用?_易语言还是python适合辅助

redis watch使用场景_详解redis中的锁以及使用场景-程序员宅基地

文章浏览阅读134次。详解redis中的锁以及使用场景,指令,事务,分布式,命令,时间详解redis中的锁以及使用场景易采站长站,站长之家为您整理了详解redis中的锁以及使用场景的相关内容。分布式锁什么是分布式锁?分布式锁是控制分布式系统之间同步访问共享资源的一种方式。为什么要使用分布式锁?​ 为了保证共享资源的数据一致性。什么场景下使用分布式锁?​ 数据重要且要保证一致性如何实现分布式锁?主要介绍使用redis来实..._redis setnx watch

推荐文章

热门文章

相关标签