为了面试而战--网络编程
学习一下新的知识-网络编程,这也是面试常考的内容。
网络编程
什么是网络编程呢?定义为在网络通信协议下,不同计算机上运行的程序,进行的数据传输。说的不像人言,根本听不懂。简单来说就是我们之前写的程序都是单机版的,只能自己玩自己的,如果你想把计算的结果或者收藏的图片视频通过网络传递给你的朋友,根据以前所学的是不行的,就需要用到今天学习的网络编程。应用场景我们很熟悉,比如即时通信、网友对战、金融证券、邮件等。这些不管是什么场景,都是计算机跟计算机之间通过网络进行数据传。
可以通过java.net包下的技术开发网络应用程序。现在市面上常见的软件架构有两种,分别是BS和CS。
C/S:Client/Server 客户端/服务器。在用户本地需要下载并安装客户端程序(qq,steam),在远程有一个服务器端程序。
B/S:Browser/Server 浏览器/服务器。只需要一个浏览器,用户通过不同的网址,用户不需要下载客户端(京东、淘宝)。客户访问不同的服务器。现在像京东、淘宝都是两种架构,既可以在浏览器上访问,也可以在手机上下载客户端。
C/S是通过客户端访问服务器,B/S为通过浏览器访问服务器。所以不管是客户端还是浏览器,他俩只负责把数据展示给用户看,项目中真正的核心都在服务器。
市场上所有的通过网页访问的都是B/S,比如网页游戏。不需要下载客户端,非常方便,但是画面真的差,很烂,因为他是通过网络传输图片和音乐的,图片内存大,网络再不好,那就没法显示图片。B/S架构的优缺点就很明显了,优点就是不需要开发客户端,只需要页面+服务端,非常容易开发和维护,而且用户也不用下载客户端,打开浏览器就能使用,非常方便。缺点也很明显,如果应用过大,用户体验就受到影响。
C/S就是LOL、吃鸡这种大型游戏,画面精美,安装包大,因为安装包中就是用户用到的图片和音乐,安装的时候就已经在用户本地了,玩游戏的过程中就不需要通过网络传输,只要告诉客户端该显示哪张图片即可。CS优点就是画面可以做的精美,用户体验好。缺点就是既要开发客户端,又要开发服务端,开发安装部署维护很麻烦。用户需要下载和更新的时候也麻烦。
网络编程三要素
两台电脑传输数据,需要知道哪些东西才能传输呢,特别是想给一对电脑发送数据。
先要确定对方电脑在互联网上的地址(IP),是唯一的标识。其次是对方电脑接收数据的软件(端口号),一个端口号只能被一个软件绑定使用。最后要确定网络传输的规则,叫做协议。所以三要素就是IP、端口号、协议。
IP:设备在网络中的地址,是唯一的标识。这里说的是设备而不是电脑,只要能上网的都有IP
端口号:应用程序在设备中唯一的标识
协议:数据在网络中传输的规则。常见协议由UDP、TCP、http、https、ftp
不用刻意背,有个印象。比如我要一堆妹子中选一个,那我是不是得确定这个妹子的位置,也就是IP,第二步就是要确定妹子用的聊天软件,是微信啊还是qq,第三步就是用我们的直男思维,去聊天,也就是我们聊天的规则。
IP
全称:Internet Protocol,是互联网协议地址,也称IP地址,是分配给上网设备的数字标签。常见分类有:IPv4和IPv6。
IPv4:互联网通信协议第四版,采取32位地址长度,分成4组。比如11000000 10101000 00000001 01000010,这要是当成IP我直接疯,所以有了点分十进制表示法,把8bit分为一组,一共四组,每一组转成十进制,变成192.168.1.66,没有负数,每一组最大值为255。这种方式有弊端,总共才有不到43亿个IP,数量不够使用。为了数量够用,出现了IPv6。
IPv6:互联网通信协议第六版,采用128位为地址长度,分成8组。最多会有2^128个IP。表示方式为冒分十六进制表示法,是把每一组转成16进制,再用:分隔,比如2001:0DB8:0000:0023:0008:0800:200C:417A,可以省略前面的0,变成2001:DB8:0:023:8:200C:417A。
IPv4
IPv4的地址值是不够用的,并且在2019年已经分配完了,但是IPv6还没有大量普及,那是怎么解决IP不够的问题呢?这里我们就要先了解一下IPv4的地址分类形式。
- 分为公网地址(万维网使用)和私有地址(局域网使用)
- 192.168.开头的就是私有地址,范围记为192.188.0.0--192.168.255.255,专门为组织机构内部使用,以此节省IP
网吧里有很多电脑,但是不是每一台电脑都有公网的IP,往往是共享同一个公网IP,再由路由器给每一台电脑一个局域网IP,就可以实现节约IP的作用。
一个特殊的IP是127.0.0.1,为本机IP,永远只会寻找当前所在本机。
那这里就有一个疑问了,假设192.168.1.100是我电脑的IP,那么这个IP跟127.0.0.1是一样的吗?答案是不一样的。假设局域网中有六台电脑,IP都是由路由器所分配的。假设现在我自己的电脑是192.168.1.100,现在要往自己电脑里发数据,数据要先发送到路由器,路由器再找IP,才能完成数据的发送。但是有个小细节,每个路由器给你分配的IP是不一样的,教室里的给你分一个,回宿舍了宿舍的又分一个,所以换一个地方上网,局域网IP可能会不一样,自己练习的时候,自己给自己发送数据,IP就写127.0.0.1就可以。
常见的CMD命令
- ipconfig:查看本机IP地址
- ping:检查网络是否连通 后面可以跟随IP也可以跟随网址,表示电脑和网址之间的网络是否畅通
InteAddress
我们之前学习了IP的知识,现在我们来学习用来表示IP的类,InteAddress。这个类的对象就是IP的对象,那有一个问题就是IP有两种,IPv4和IPv6,到底是哪一个对象呢?InteAddress有两个子类,一个是Inte4Address,一个是Inte6Address,获取这个类对象的时候底层会判断你当前系统是4版本还是6版本,对应创建不同的子类。如何获取对象呢?这个类没有构造方法,不能通过new的方式创建对象,要通过其静态方法获取对象,这个方法底层就做了判断,是IPv4还是IPv6。
public class MyInetAddressDemo1 { public static void main(String[] args) throws UnknownHostException { /* static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址 String getHostName() 获取此IP地址的主机名 String getHostAddress() 返回文本显示中的IP地址字符串 */ //1.获取InetAddress的对象 //IP的对象 一台电脑的对象 InetAddress address = InetAddress.getByName("DESKTOP-5OJJSAM"); System.out.println(address); String name = address.getHostName(); System.out.println(name);//DESKTOP-5OJJSAM String ip = address.getHostAddress(); System.out.println(ip);//192.168.1.100 } }
上面代码是前置代码,当我们获取到某一台电脑的对象后,不就可以给电脑发送信息了吗。
端口号
应用程序在设备中唯一的标识。端口号由两个字节表示的整数,取值范围是0~65535,但并不是所有端口号都能用,其中0~1023之间的端口号用于一些知名的网络服务或者应用,简单来说我们只能用1024之后的,而且一个端口号只能被一个应用程序所使用。那端口该怎么理解呢?
现在我们有两台电脑,电脑A和电脑B,我们可以把端口理解为往外发送数据或者接受数据的出口和入口,这些口都是有编号的,从0到65535,而我们的软件在启动后,一定要和一个端口绑定。如果不绑定,启动后是单机的,无法对外发送数据或者接收数据。比如电脑A的微信端口号是65533,他发送数据到电脑B的微信端口,同样也是65533,这样两个就可以互联了。
协议
协议是网络编程三要素中最后一个。在计算机网络中,连接和通信的规则被称为网络通信协议,说白了协议就是数据传输的规则。目前是国际标准的协议为TCP/IP参考模型(或者称为TCP/IP协议),OSI没能广泛推广。
将整个数据的传输分成5层,发送数据的时候对方电脑也有这5层。发送数据的时候会一层一层往下传输数据,在最下面会把数据转成2进制,再把数据传给对方电脑。对方电脑接收到后,会进行解析,再一层一层的返回到应用层。每一层都有自己的协议,应用层协议底层就是TCP、UDP。
先来学习两个协议,分别是UDP协议和TCP协议
UDP协议是用户数据报协议(User Datagram Protocol),特点是面向无连接通信协议。速度快,有大小限制,一次最多法送64K,数据不安全,易丢失数据。什么叫面向无连接呢?就是说有两台电脑,一台往另一台发送数据,正常应该是要检查一下两台电脑是否网络连接成功,成功再发送,UDP则不会管你连没连网,直接就发送,能收到就收到,收不到拉倒。
TCP协议又叫传输控住协议TCP(Transmission Control Protocol),TCP协议是面向连接的通信协议,速度慢,没有大小限制,数据安全。刚好了UDP相反,发送数据之前要检查网络是否连接。
UDP适用于丢失一点数据不会有影响的情况,比如网络会议、语音通话、在线视频。TCP适用于数据一点不能丢的情况,比如下载软件、在线聊天。
UDP通信程序
现在我们来学习如何利用UDP来发送接收数据,这个过程比较繁琐,用一个故事来讲解。比如我要送我女朋友礼物,比如小熊、裙子、辣条。但是我在杭州,女朋友在沈阳,我该怎么送呢?是不是只能通过快递给呀。那我肯定不能把这些礼物散装发过去吧,就得用一个箱子把他们装进去并打包好,交给快递小哥寄出去,这就是发送。在这个过程中,我做了四件事,包括:
- 找快递公司 创建发送端DatagramSocket对象
- 打包礼物 数据打包(DatagramPacket)
- 快递公司发货 发送数据
- 我付钱走人 释放资源
以上四步也对应UDP发送数据的四步
public class SendMessageDemo { public static void main(String[] args) throws IOException { //发送数据 //1.创建DatagramStock对象(快递公司) //小细节 //绑定端口,以后我们就通过这个端口往外发送 //空参:所有可用的端口中随机绑定一个使用 //有参:指定端口号进行绑定 DatagramSocket ds = new DatagramSocket(); String str = "我好帅啊"; //把str转成字节数组 bytep[] bytes = str.getBytes();] //指定电脑,要往哪里发,通过InteAddress获取IP对象 InteAddress address = InteAddress.getByName("127.0.0.1"); //指定端口号,你要往哪个端口里发送 int port = 10086; //2.打包数据 第二个参数表示要把字节数组里的所有参数都发出去,往address这台电脑发,发到port端口。 DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port); //3.发送数据 DatagramSocket对象调用send方法把包发送出去 ds.send(dp); //4.释放资源 ds.close(); } }
已经学完UDP发送数据,现在来学习一下怎么接受数据。现在是我女朋友接收包裹,她该怎么办呢?首先快递公司肯定是要把包裹交给她,然后取出我送的礼物,这就是接收。这个过程有一下几个步骤:
- 找快递公司 创建接收端的DatagramSocket对象
- 接收箱子 接收打包好的数据
- 从箱子里获取礼物 解析数据包
- 签收走人 释放资源
一定要先发送,再接收。
public class ReceiveMessageDemo { public static void main(String[] args) throws IOException { //1.创建DatagramSocket对象(快递公司) //细节: //在接收的时候一定要绑定端口,而且绑定的端口要跟发送的端口保持一致 DatagramStocket ds = new DatagramStocket(10086); byte[] bytes = new byte[1024]; //2.接收数据包,接收的时候就不用后两个参数,端口号和IP,我只需要一个接收数组,把接收的数据放到这个数组中。bytes.length表示接收所有数组,如果是10的话就是接收bytes的前10个数据 DatagramPacket dp = new DatagramPacket(bytes,bytes.length); //该方法是阻塞的 //程序执行到这一步的时候,会在这里死等 //等发送端发送消息 ds.receive(dp); //3.解析数据包 获取的就是上面的数组 byte[] data = dp.getData(); //获取到多少个字节数据呀 int len = dp.getLength(); System.out.println("接收到数据"+new String(data,0,len)); //还能知道是从哪台电脑发来的 InetAddress address = dp.getAddress(); String ip = dp.getHostAddress(); String name = dp.getHostName(); //知道从哪个端口发来的 int port = dp.getPort(); //4.释放资源 ds.close(); } }
UDP的三种通信方式
UDP在通信的时候有三种方式,分别是单播、组播、广播。
先看第一种,单播。所谓单播,就是一对一,左边发送端只给右边一台电脑发送。第二种为组播,左边一台电脑可以给右边一组电脑发送。第三种为广播,左边的一台电脑可以给右边局域网内所有电脑发送数据。
代码怎么写呢?以前的代码就是单播。组播要有组播地址,地址为224.0.0.0~239.255.255.255,其中224.0.0.0~224.0.0.255为预留的组播地址。我们相拥就只能用预留地址,这跟之前的IP区别就在于之前我们用IP只能表示一台电脑,现在组播地址可以表示多台电脑。广播呢就是我们发送信息到255.255.255.255上,那此时在局域网里所有电脑都能收到。
public class SendMessageDemo { public static void main(String[] args) throws IOException { /* 组播发送端代码 */ //创建MulticastSocket对象 MulticastSocket ms = new MulticastSocket() ; // 创建DatagramPacket对象 String s = "你好,你好!" ; byte[] bytes = s.getBytes(); //指定IP的时候要指定组播地址 InetAddress address = InetAddress.getByName("224.0.0.1"); int port = 10000; DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port) ; // 调用MulticastSocket发送数据方法发送数据 ms.send(datagramPacket); // 释放资源 ms.close(); } } public class ReceiveMessageDemo1 { public static void main(String[] args) throws IOException { /* 组播接收端代码 */ //1. 创建MulticastSocket对象 MulticastSocket ms = new MulticastSocket(10000); //2. 将将当前本机,添加到224.0.0.1的这一组当中 InetAddress address = InetAddress.getByName("224.0.0.1"); ms.joinGroup(address); //3. 创建DatagramPacket数据包对象 byte[] bytes = new byte[1024]; DatagramPacket dp = new DatagramPacket(bytes, bytes.length); //4. 接收数据 ms.receive(dp); //5. 解析数据 byte[] data = dp.getData(); int len = dp.getLength(); String ip = dp.getAddress().getHostAddress(); String name = dp.getAddress().getHostName(); System.out.println("ip为:" + ip + ",主机名为:" + name + "的人,发送了数据:" + new String(data,0,len)); //6. 释放资源 ms.close(); } } public class ReceiveMessageDemo2 { public static void main(String[] args) throws IOException { /* 组播接收端代码 */ //1. 创建MulticastSocket对象 MulticastSocket ms = new MulticastSocket(10000); //2. 将将当前本机,添加到224.0.0.1的这一组当中 InetAddress address = InetAddress.getByName("224.0.0.1"); ms.joinGroup(address); //3. 创建DatagramPacket数据包对象 byte[] bytes = new byte[1024]; DatagramPacket dp = new DatagramPacket(bytes, bytes.length); //4. 接收数据 ms.receive(dp); //5. 解析数据 byte[] data = dp.getData(); int len = dp.getLength(); String ip = dp.getAddress().getHostAddress(); String name = dp.getAddress().getHostName(); System.out.println("ip为:" + ip + ",主机名为:" + name + "的人,发送了数据:" + new String(data,0,len)); //6. 释放资源 ms.close(); } } public class ReceiveMessageDemo3 { public static void main(String[] args) throws IOException { /* 组播接收端代码 */ //1. 创建MulticastSocket对象 MulticastSocket ms = new MulticastSocket(10000); //2. 将将当前本机,添加到224.0.0.2的这一组当中 InetAddress address = InetAddress.getByName("224.0.0.2"); ms.joinGroup(address); //3. 创建DatagramPacket数据包对象 byte[] bytes = new byte[1024]; DatagramPacket dp = new DatagramPacket(bytes, bytes.length); //4. 接收数据 ms.receive(dp); //5. 解析数据 byte[] data = dp.getData(); int len = dp.getLength(); String ip = dp.getAddress().getHostAddress(); String name = dp.getAddress().getHostName(); System.out.println("ip为:" + ip + ",主机名为:" + name + "的人,发送了数据:" + new String(data,0,len)); //6. 释放资源 ms.close(); } }
我们只需要在单播代码中,将指定的IP改成“255.255.255.255”就可以广播了。
TCP通信程序
之前我们学完了UDP通信协议,现在我们开始学习TCP协议。TCP通信协议是一种可靠地网络协议,它在通信的两端各建立一个Socket对象,通信之前要保证连接已经建立。连接建立之后会通过IO流进行网络通信,通过Socket产生IO流。
举个例子,有两台电脑,左边的是发送端,也叫客户端(Socket),右边的是接收端,叫做服务器(ServerSocket)。他们两个在发送数据之前,一定要保证连接已经建立,如果说连接不建立,TCP是无法发送数据的。对于客户端,他是往外发,是输出流,对于接收端,是往里收,是输入流。
客户端这个过程包括如下几个步骤:
- 创建客户端的Socket对象(Socket)与指定服务端连接 Socket(String host,int port)
- 获取输出流,写数据 OutputStream getOutputStream()
- 释放资源 void close()
再看服务器的过程,包括以下几个步骤:
- 创建服务器端的Socket对象(ServerSocket) ServerSocket(int port),这里的port要和客户端的port保持一致。
- 监听客户端连接,返回一个Socket对象。 Socket accept(),这就表示连接已经建立
- 获取输入流,读数据,并把数据显示在控制台 InputStream getInputStream()
- 释放资源 void close()
public class Client { public static void main(String[] args) throws IOException { //TCP,发送数据 //1.创建socket对象 //细节:在创建对象的同时会连接服务器,如果连接不上会报错 Socket socket = new Socket("127.0.0.1",10000); //2.可以从连接通道中获取输出流 OutputStream os = socket.getOutputStream(); //写出数据,字节流写出的时候只能写字节信息 os.write("你好你好",getBytes()); os.close(); socket.close(); } } public class Server { public static void main(String[] args) throws IOException { //TCP协议:接收数据 //1.创建对象ServerSocket ServerSocket ss = new ServerSocket(10000);//绑定端口,和客户端一致 //2.监听客户端的连接,其实就是调用accept()方法死等,要是一致没有客户端连接,这边就会卡死在这里。如果客户端连接,那accept()就会返回客户端对象 Socket socket = ss.accept(); //3.从连接通道中输入流 InputStream is = socket.getInputStream(); int b; while(b = is.read() != -1){ System.out.print((char) b); } //4.释放资源 socket.close();//断开了和客户端的连接 ss.close();//关闭服务器 } }
结果就是乱码了,因为发送的是中文,现在我们就要来解决这个问题。为什么会出现乱码呢?在客户端我们没有指定编码表,所以会使用IDEA默认的编码表UTF-8,此时会把一个中文变成三个字节,所以代码中就会把“你好你好”转成12个字节全都发送到服务器中。但是在服务端,他是一个字节一个字节读的,读一个字节就转成字符,也就是说每次转的就是三分之一个字符,必乱码。右边服务器要改动一下,不能用字节流读,要结合编码表读。
public class Server { public static void main(String[] args) throws IOException { //TCP协议:接收数据 //1.创建对象ServerSocket ServerSocket ss = new ServerSocket(10000);//绑定端口,和客户端一致 //2.监听客户端的连接,其实就是调用accept()方法死等,要是一致没有客户端连接,这边就会卡死在这里。如果客户端连接,那accept()就会返回客户端对象 Socket socket = ss.accept(); //3.从连接通道中输入流 /* InputStream is = socket.getInputStream(); //转换流 把is变成字符流 InputStreamReader isr = new InputStreamReader(is); //为了提高读取效率,我们可以在外边包一个缓冲流 BufferedReader br = new BufferedReader(isr); */ //写成一行 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); int b; while(b = isr.read() != -1){ System.out.print((char) b); } //4.释放资源 socket.close();//断开了和客户端的连接 ss.close();//关闭服务器 } }
我们已经学完了TCP协议发送和接收,现在我们来讲一讲这两部分代码的小细节。
先来想第一个问题,是客户端先运行还是服务端先运行呢?假如我们先运行了客户端,他要和127.0.0.1这台电脑的10000端口进行连接,那右边服务端还没启动呢,他跟谁去连呢?没有东西啊,所以客户端第一行狠狠地报错。我们启动程序的时候,要先启动服务端,服务端第一行代码就是跟10000端口进行绑定(三次握手协议保证连接建立,后面会讲)。然后调用accept()之后就会死等,等有客户端来连,所以这是必须要启动客户端,客户端第一行就是和服务端这台电脑的10000端口连,这是连接已经建立了,接着就返回客户端连接对象。现在客户端和服务端之间就有了传输数据通道,之后就是服务器写、客户端读数据的过程了,都是通过IO流实现,而且这个流不是我们自己创建的,而是通过socket连接通道获取出来的,流是在连接通道里面的。因为流在通道里面,通道关了流也就关了,所以不需要自己去关流,流的close可写可不写。关闭通道的时候有个小细节,假如数据还在通道里传呢,没传过去,这时候要是关了通道肯定是不合理的。所以说我在断开服务器要把通道里的数据都处理完毕了,才能关闭socket通道。断开里还有一个协议叫做四次挥手协议(后面讲),利用这个协议断开连接,保证连接通道里的数据已经处理完毕。
TCP通信程序(三次握手)
现在我们来学习上文提到的三次握手和四次挥手协议,都是干什么的呢。
先来学习第一个,三次握手。三次握手是用来确保连接建立,客户端向服务器发送连接请求,等到服务器确认,所以这个请求不是说连就连的,需要等待确认。此时服务器会返回给客户端一个响应,告诉客户端收到了你发来的请求,现在我允许你连接。此时客户端再次发送确认信息,此时连接正式建立。所以为什么称之为三次握手呢,就是有反复确认的过程。
TCP通信程序(四次挥手)
四次挥手用来确保连接断开,且数据处理完毕。首先客户端向服务端发送取消连接请求,服务器收到后向客户端返回一个响应,表示已经收到客户端取消请求。但是你别急,通道里还有数据没处理呢,所以要等待服务器把通道中剩下的数据处理完。处理完之后会再次发送确认取消信息给客户端,客户端会再次确认消息,取消连接。这就是四次挥手。
练习题
大伙来做几道小练习吧。
1.接收并反馈
这个练习需要注意的是反馈过程,反馈就是指服务器发送数据给客户端。这个过程中服务器为输出流,客户端为输入流,这样就需要各自创建流对象,发送接收数据。但是服务器再反馈的过程中会出现问题。假如我们下面代码,不写第18行的客户端结束标记,会发生什么呢?运行程序之后,发现客户端发送和服务器接收都是没问题的,但是客户端没有收到服务器的反馈,这是为什么呢?因为服务器再循环的时候,一直在等待客户端发送数据,如果客户端不发送数据,那服务器就会停留在while循环里,不会向下执行,所以无法将数据反馈回客户端。因此我们要在客户端写一个结束标记,这就可以了,那我学到这时候就有疑问了,好像之前咱也没弄结束标记,为什么还是结束运行程序了呢?因为我们之前的客户端直接关闭了传输通道,所以就不会有数据传输,自然程序就结束运行了,现在我们看一下客户端代码,是不是还有个循环等着呢,这就不会执行到close()了。
结束标记为:socket.shutdownOutput();
public class Client { public static void main(String[] args) throws IOException { //客户端:发送一条数据,接收服务端反馈的消息并打印 //服务器:接收数据并打印,再给客户端反馈消息 //1.创建Socket对象并连接服务端 Socket socket = new Socket("127.0.0.1",10000); //2.写出数据 String str = "见到你很高兴!"; OutputStream os = socket.getOutputStream(); os.write(str.getBytes()); //写出一个结束标记 socket.shutdownOutput(); //3.接收服务端回写的数据 InputStream is = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(is); int b; while ((b = isr.read()) != -1){ System.out.print((char)b); } //释放资源 socket.close(); } } public class Server { public static void main(String[] args) throws IOException { //客户端:发送一条数据,接收服务端反馈的消息并打印 //服务器:接收数据并打印,再给客户端反馈消息 //1.创建对象并绑定10000端口 ServerSocket ss = new ServerSocket(10000); //2.等待客户端连接 Socket socket = ss.accept(); //3.socket中获取输入流读取数据 InputStream is = socket.getInputStream(); InputStreamReader isr = new InputStreamReader(is); int b; //细节: //read方法会从连接通道中读取数据 //但是,需要有一个结束标记,此处的循环才会停止 //否则,程序就会一直停在read方法这里,等待读取下面的数据 while ((b = isr.read()) != -1){ System.out.println((char)b); } //4.回写数据 String str = "到底有多开心?"; OutputStream os = socket.getOutputStream(); os.write(str.getBytes()); //释放资源 socket.close(); ss.close(); } }
2.上传文件
public class Client { public static void main(String[] args) throws IOException { //客户端:将本地文件上传至服务器,接收服务器的反馈信息 //服务器:接收客户端上传的文件,上传完毕后给出反馈 //1.创建Socket对象,并连接服务器 Socket socket = new Socket("127.0.0.1",10000); //2.读取本地文件中的数据,并写到服务器中 //本地文件比较大,不能用FileInputStream一个字节那么读,效率差。 BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\clientdir\a.jpg")); BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream()); byte[] bytes = new byte[1024]; int len; while((len = bis.read(bytes) != -1){ bos.write(bytes,0,len); } //往服务器写出结束标记 socket.shutdownOutput(); //3.接收回显数据 //只有一句话,没必要那么复杂,用BufferedReader中的readLine()方法读一整行 //socket中的输入流是字节流,再变成字符流,最后变成缓冲流。 BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); //读一次就ok啦 String line = br.readLine(); //4.释放资源 socket.close(); } } public class Server { public static void main(String[] args) throws IOException { //1.创建对象并绑定端口 ServerSocket ss = new ServerSocket(10000); //2.等待客户端来连接 Socket socket = ss.accept(); //3.读取数据并保存到本地文件中 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); //跟本地文件关联 BufferedOutputStream bos = new BufferedInputStream(new FileOutputStream("mysocketnet\serverdir\a.jpg")); int len; byte[] bytes = new byte[1024]; while((len = bis.read(bytes)) != -1){ bos.write(bytes,o,len); } //4.回显数据 //从连接通道中得到字节流,转成字符流,最后转成缓冲流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream)); //直接写出中文 bw.writer("上传成功"); bw.newLine(); bw.flush(); //3.释放资源 socket.close(); ss.close(); } }
这个练习还有一点问题没解决,就是你在服务端生成的文件名豆角a.jpg,这必重名啊,我就想让每个文件名子是不一样的。在Java中有一个类是专门做这件事的,叫UUID,通用唯一标识符类,简单来说会随机生成一个字符串,而且是唯一的,所以我就可以用这个类随机生成文件名。看看这个类怎么用呢?
public class UUIDTest { public static void main(String[] args) { String str = UUID.randomUUID().toString().replace("-", ""); System.out.println(str);//9f15b8c356c54f55bfcb0ee3023fce8a } }
public class Server { public static void main(String[] args) throws IOException { //1.创建对象并绑定端口 ServerSocket ss = new ServerSocket(10000); //2.等待客户端来连接 Socket socket = ss.accept(); //3.读取数据并保存到本地文件中 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); String name = UUID.randomUUID().toString().replace("-", ""); //跟本地文件关联 BufferedOutputStream bos = new BufferedInputStream(new FileOutputStream("mysocketnet\serverdir\" + name + ".jpg")); int len; byte[] bytes = new byte[1024]; while((len = bis.read(bytes)) != -1){ bos.write(bytes,o,len); } //4.回显数据 //从连接通道中得到字节流,转成字符流,最后转成缓冲流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream)); //直接写出中文 bw.writer("上传成功"); bw.newLine(); bw.flush(); //3.释放资源 socket.close(); ss.close(); } }
3.上传文件(多线程版)
想要服务器不停止,能接收很多用户创传的图片。该怎么做呢?就用多线程来做。也可以用循环,但是有弊端
public class Server { public static void main(String[] args) throws IOException { //1.创建对象并绑定端口 ServerSocket ss = new ServerSocket(10000); while(true){ //2.等待客户端来连接 Socket socket = ss.accept(); //3.读取数据并保存到本地文件中 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); String name = UUID.randomUUID().toString().replace("-", ""); //跟本地文件关联 BufferedOutputStream bos = new BufferedInputStream(newFileOutputStream("mysocketnet\serverdir\" + name + ".jpg")); int len; byte[] bytes = new byte[1024]; while((len = bis.read(bytes)) != -1){ bos.write(bytes,o,len); } //4.回显数据 //从连接通道中得到字节流,转成字符流,最后转成缓冲流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream)); //直接写出中文 bw.writer("上传成功"); bw.newLine(); bw.flush(); //3.释放资源 socket.close(); } } }
我们来看一下循环代码会有什么样的问题,比如我们开启了多个客户端,当第一个客户端和服务器建立连接后,传文件的时候是不是会进入while循环,如果这个文件没传完是不是就不会退出while循环,那假如我们第二个客户端此时也要和服务器建立连接,那肯定连不上了,必须要等第一个完事儿才行。这就是循环的弊端,同样也是单线程的弊端,单线程只能一个一个来。这不是我想要的,我想要服务器能同时被多个客户端上传,怎么办呀?只能用多线程。
public class Server { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(10000); while(true){ //等待客户端来连接 Socket socket = ss.accept(); //开启一条线程 //有一个客户端就对应服务端的一条线程 new Thread(new MyRunnable(socket)).start(); } } } public MyRunnable implements Runnable{ Socket socket; //通过构造方法传递Socket对象 public MyRunnable(Socket socket){ this.socket = socket; } public void run(){ try { //3.读取数据并保存到本地文件中 BufferedInputStream bis = new BufferedInputStream(socket.getInputStream()); String name = UUID.randomUUID().toString().replace("-", ""); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg")); int len; byte[] bytes = new byte[1024]; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } bos.close(); //4.回写数据 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write("上传成功"); bw.newLine(); bw.flush(); } catch (IOException e) { e.printStackTrace(); } finally { //5.释放资源 if(socket != null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
我们之前学习线程的时候也提到过,频繁的创建线程并销毁非常浪费系统资源,所以需要用线程池优化。
public class Server { public static void main(String[] args) throws IOException { //创建线程池对象 ThreadLocalExecutor pool = new ThreadLocalExcutor( 3,//核心线程数量 16,//线程池总大小 60,//空闲时间 TimeUnit.SECONDS,//空闲时间单位 new ArrayBlockingQueue<>(2),//队列 Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象 new ThreadPoolExecutor.AbortPolicy()//阻塞队列 ); //1.创建对象并绑定连接 ServerSocket ss = new ServerSocket(10000); while(true){ //等待客户端连接 Socket socket = ss.accpet(); pool.submit(new MuRunnable(socket)); } } }
以上就是网络编程的想过知识啦,希望能对大伙学习有帮助,24届的小伙伴们我们一起进步一起拿offer。
#你觉得今年春招回暖了吗##数据人的面试交流地##远程面试的尴尬瞬间#我是一个转码的小白,平时会在牛客中做选择题,在做题中遇到不会的内容就会去找视频或者文章学习,以此不断积累知识。这个专栏主要是记录一些我通过做题所学到的基础知识,希望能对大家有帮助