网络传输基础—————简易聊天室Java实现篇

浅析TCP/UDP

CS/BS

C = Client, S = Server。C/S 架构即“客户端-服务器” 架构。这里的“客户端”可以是有 GUI (图形用户界面)的定制软件,也可以是浏览器,甚至可以是通过 SSH 访问服务器的命令行脚本。只要是客户端通过访问服务器调取计算或者存储资源的,统统都是 C/S 架构。所谓的 Browser-Server 架构其实是 C/S 架构的一种特殊的实现形式,而不是其对立面。

TCP/UDP

TCP的优点: 可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。 TCP的缺点: 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。

UDP的优点: 快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击…… UDP的缺点: 不可靠,不稳定 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。 基于上面的优缺点,那么: 什么时候应该使用TCP: 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输 ………… 什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频

编程区别

通常我们在说到网络编程时默认是指TCP编程,即用前面提到的socket函数创建一个socket用于TCP通讯,函数参数我们通常填为SOCK_STREAM。即socket(PF_INET, SOCK_STREAM, 0),这表示建立一个socket用于流式网络通讯。
  SOCK_STREAM这种的特点是面向连接的,即每次收发数据之前必须通过connect建立连接,也是双向的,即任何一方都可以收发数据,协议本身提供了一些保障机制保证它是可靠的、有序的,即每个包按照发送的顺序到达接收方。

而SOCK_DGRAM这种是User Datagram Protocol协议的网络通讯,它是无连接的,不可靠的,因为通讯双方发送数据后不知道对方是否已经收到数据,是否正常收到数据。任何一方建立一个socket以后就可以用sendto发送数据,也可以用recvfrom接收数据。根本不关心对方是否存在,是否发送了数据。它的特点是通讯速度比较快。大家都知道TCP是要经过三次握手的,而UDP没有。

基于上述不同,UDP和TCP编程步骤也有些不同,如下:

TCP:
TCP编程的服务器端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt(); * 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();
  4、开启监听,用函数listen();
  5、接收客户端上来的连接,用函数accept();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接;
  8、关闭监听;

TCP编程的客户端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt();* 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
  4、设置要连接的对方的IP地址和端口等属性;
  5、连接服务器,用函数connect();
  6、收发数据,用函数send()和recv(),或者read()和write();
  7、关闭网络连接;

UDP:
与之对应的UDP编程步骤要简单许多,分别如下:
  UDP编程的服务器端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt();* 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();
  4、循环接收数据,用函数recvfrom();
  5、关闭网络连接;

UDP编程的客户端一般步骤是:
  1、创建一个socket,用函数socket();
  2、设置socket属性,用函数setsockopt();* 可选
  3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
  4、设置对方的IP地址和端口等属性;
  5、发送数据,用函数sendto();
  6、关闭网络连接;

TCP的具体代码实现:

聊天室客户端

  • @author EP
  • @date 2020年3月25日
  • @version 1.0
package socket;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Socket;
import java.util.Scanner;
import java.util.logging.Handler;

/**
 * 聊天室客户端
* @author EP
* @date 2020年3月25日  
* @version 1.0
 */

public class Client {
	/*
	 * java.net.Socket 套接字
	 * Socket封装了TCP协议的通讯细节,使得我们使用它与服务器建立TCP链接后,可以
	 * 以两个流(一个输入流,一个输出流)的读写完成与服务器的数据相互传输。
	 */
	
	private Socket socket;
	/**
	 * 构造方法,用于初始化客户端程序 
	 */
	public Client() {
		try {
			/*
			 * 实例化Socket时需要传入两个参数
			 * 参数1:远端计算机的地址信息(服务端的IP地址)
			 * 参数2:远端计算机开启的端口
			 * 通过IP地址可以找到网络中的服务端计算机,通过端口可以连接到
			 * 该机器上运行的服务端应用程序。
			 * 
			 * 实例化的过程就是建立连接的过程,如果服务器无响应,这里会抛出异常
			 */
			System.out.println("正在连接服务器...");
			//通过cmd ipconfig查,或者直接网络连接里面的IPv4
			socket = new Socket("192.168.1.3",8088);  //也可以用localhost连接本机
			System.out.println("与服务器建立连接!");
		} catch (IOException e) {
			
			e.printStackTrace();
		}
		
	}
	/**
	 * 启动方法,用于开始客户端操作
	 */
	
	public void start() {
		try {
			//先启动用于读取服务端发送过来信息的线程
			ServerHandler handler = new ServerHandler();
			Thread t = new Thread(handler);
			t.start();
			/*
			 * Socket提供的方法:
			 * OutputStream getOutputStream()
			 * 该方法会返回一个字节输出流,通过该流写出的字节会通过网络发送给
			 * 远端计算机
			 */
			OutputStream out = socket.getOutputStream();
			OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");
			BufferedWriter bw = new BufferedWriter(osw);
			PrintWriter pw = new PrintWriter(bw,true);
			

			Scanner scanner = new Scanner(System.in);
			while (true) {
				System.out.println("请输入内容。");
				String str = scanner.nextLine();
				pw.println(str);	
				
			}
			
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	public static void main(String[] args) {
		Client client = new Client();
		client.start();
	}
	/*
	 * 该线程负责读取服务端发送过来的消息
	 */
	private class ServerHandler implements Runnable{
		@Override
		public void run() {
			try {
				//通过socket获取输入流,读取服务端发来的信息
				InputStream in = socket.getInputStream();
				InputStreamReader isr = new InputStreamReader(in, "utf-8");
				BufferedReader br = new BufferedReader(isr);
				
				String message ="";
				while ((message=br.readLine())!=null) {
					System.out.println(message);
				}
				
			} catch (Exception e) {
				e.printStackTrace();
			}
			
			
		}
	}


	
	
	
	
	
	
	

}

聊天室服务器

  • @author EP
  • @date 2020年3月25日
  • @version 1.0
package socket;
/**
 * 聊天室服务器
* @author EP
* @date 2020年3月25日  
* @version 1.0
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;

public class Server {
	private ServerSocket server;
	/*
	 * 要想实现客户端之间转发消息,就要让所有ClientHandler能够互访对方的
	 * 输出流,才有机会将自己客户端发送过来的消息转发给其他客户端。
	 * 而ClientHanler如果想将自己的输出流共享给其他ClientHandler实例,那么
	 * 就要将它放在所有ClientHandler实例都能看得到的地方。
	 * 而java中内部类是可以访问外部类的属性的,因此在Server上定义一个属性,
	 * 那么所有内部类实例ClientHandler都可以访问到,因此在Server上定义一个数组
	 * 存放所有ClientHandler需要共享的输出流就可以了。
	 */
	
	//存放所有客户端输出流,用于广播消息
	private PrintWriter[]allOut= {};
	
	public Server() {
		try {
			/*
			 * java.net.ServerSocket
			 * 运行在服务端的ServerSocket主要有两个工作:
			 * 1:向系统申请服务端口,客户端就是通过这个端口与服务端建立连接的
			 * 2:监听服务端口,一旦一个客户端与之连接,则会创建一个Socket,通过
			 * 这个Socket就可以与客户端交互了。
			 * 
			 * 实例化的过程如果抛出下面的异常:
			 * java.net.BindException:Address already in use:JVM_Bind
			 * 说明该端口已经被系统其它程序使用了,通常情况是我们写的服务端重复
			 * 启动了两次造成的。先把之前的服务端关闭即可。
			 */
			System.out.println("正在启动服务端...");
			server = new ServerSocket(8088);
			System.out.println("服务端启动完毕!");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void start() {
		try {
			/*
			 * Socket accept()
			 * 该方法是一个阻塞方法,调用后会开始等待客户端的连接,一旦一个客户端
			 * 建立连接,则该方法会立即返回一个Socket实例,通过这个Socket即可以
			 * 与该客户端进行交互了。
			 * 
			 * 重复调用accept则可以接受多个客户端的连接
			 */
			
			while (true) {
				System.out.println("等待客户端连接...");
				Socket socket = server.accept();   //类似于Scanner,何时接受取决于客户端的发起
				System.out.println("一个客户端连接了!");	
				/*
				 * 每当一个客户端连接后,就实例化一个线程,并将对该客户端的
				 * Socket传入线程,让线程处理与该客户端的交互
				 * 
				 */
				
				Runnable handler = new ClientHandler(socket);
				Thread t = new Thread(handler);
				t.start();
				
				
			}
			
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	
	public static void main(String[] args) {
		Server server = new Server();
		server.start();
	}
	
	/*
	 * 该线程的任务是与指定客户端进行交互工作
	 */
	private class ClientHandler implements Runnable{   //定义成内部类是有原因的,即可以访问外部类的数据,多个内部类可以互通
		private Socket socket;
		private String host;//客户端的IP地址信息
		public ClientHandler(Socket socket) {  //创建构造方法把socket传进来
			this.socket = socket;
			//通过socket获取远端计算机的地址信息的字符串格式
			host = socket.getInetAddress().getHostAddress();
		}

		@Override
		public void run() {
			PrintWriter pw = null;   //关键在于pw是个局部变量,是线程里面new出来的,它存入数组里面的是引用地址
			try {			
				/*
				 * InputStream getInputStream()
				 * 通过Socket获取的输入流可以读取远端计算机发送过来的字节
				 */
					InputStream in = socket.getInputStream();
					InputStreamReader isr = new InputStreamReader(in,"utf-8");
					BufferedReader br = new BufferedReader(isr);
					
					//通过socket获取输出流,用于给客户端发送信息
					OutputStream out = socket.getOutputStream();
					OutputStreamWriter osw = new OutputStreamWriter(out, "utf-8");
					BufferedWriter bw = new BufferedWriter(osw);
					pw = new PrintWriter(bw,true);
					/*
					 * 将当前客户端输出流存入共享数组,以便其他ClientHanler
					 * 可以通过该输出流给当前客户端发送消息
					 */
					
					synchronized (allOut) {
						//1 对allOut数组扩容
						allOut =Arrays.copyOf(allOut, allOut.length+1);
						//2 将当前客户端的输出流存入数组
						allOut[allOut.length-1]= pw;
						System.out.println(host+"上线了,当前在线人数:"+allOut.length);
						
					}
					
					//循环调用readLine()
					String message="";
					/*
					 *服务端使用缓冲字符输入流读取客户端发送一来一行字符串的readLine
					 *方法这里,当客户端断开连接时,由于客户端系统不同,服务端这个方法
					 *体现的效果也不相同:
					 *当windows的客户端断开时:readLine方法会直接抛出异常
					 *当linux的客户端断开时:readLine方法会返回null值
					 *
					 *如果客户端与服务端保持连接,而客户端不发送消息时,服务端这里读取
					 *操作的readLine方法会保持阻塞,直到客户端发送一行字符串才会将其
					 *返回。
					 * 
					 */
					while ((message=br.readLine())!=null) {      
						System.out.println(host+"说:"+message);	
						synchronized (allOut) {   //使该动作与增删操作也保持互斥
							//将消息回复给所有客户端
							for (int i = 0; i < allOut.length; i++) {
								allOut[i].println(host+"说:"+message);
							}
							
						}
						
				}
				
				
				
			} catch (Exception e) {
				
			}finally {
				//处理当前客户端断开连接的操作(这个操作是在当前线程里完成的)
				
				synchronized (allOut) {   //不仅保证使增删操作本身同步,增删操作还保证了互斥
					//将当前客户端对应的pw从共享数组中删除
					for (int i = 0; i < allOut.length; i++) {
						if (allOut[i]==pw) {
							allOut[i]= allOut[allOut.length-1];   //直接覆盖掉就行了,没必要换位置,反正是要删掉的
							allOut = Arrays.copyOf(allOut, allOut.length-1);
							break;
						}		
					}
					
					System.out.println(host+"下线了,当前在线人数:"+allOut.length);
					
				}
				
				try {
					/*
					 * 当我们关闭socket后,通过socket获取的输入流与输出流包
					 * 就关闭了。
					 */
					socket.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			
			
		}
		
	}
}

如果需要将自己的电脑设置为服务器,则需要先查到在公网上的本地IP地址,网址如下:
http://www.net.cn/static/customercare/yourip.asp
在这里插入图片描述
然后通过设置光猫和路由器的映射,即虚拟服务器,来最终连接到自己的电脑。
在这里插入图片描述
在这里插入图片描述

But,为什么天翼网关的应用界面打不开呢
在这里插入图片描述

真的是辣鸡哎~ 我这台联想y400成为服务器的梦想就这样破灭了…

总结一下

聊天室程序的实现可以分为四个步骤:

  • 1.实现单个客户端收发
  • 2.实现服务端公示并发回
  • 3.建立Arraylist集合,将pw加入集合实现所有信息的公示(集合的优点:弹性数组,增删元素方便)
  • 4.synchronized锁住集合,保证线程安全

另外,如果发生8088端口被占用,无法关停的现象,可以进入cmd控制台:
在这里插入图片描述
可以查到占用线程名:
在这里插入图片描述
再用taskkill命令关闭:
在这里插入图片描述
taskkill / f/ pid 15116

原创文章 46 获赞 7 访问量 2086

猜你喜欢

转载自blog.csdn.net/EdwardWH/article/details/105094409
今日推荐