最近在CSDN上学习了Socket通信群聊的方法,就觉得这样就可以实现QQ的样子了。然后让女朋友用电脑试了一下,运行用户端代码,发现连连接到服务器都做不到。后来经过自己的研究,实现了QQ群聊的功能,后续会发私聊功能。如果有错误的麻烦大家指正!!!
目录
2.修改服务器主机路由器中的映射关系,将内网映射到外网(不是我们想要的)
一、原因分析及解决方案
1.名词简介
1.局域网(内网)
局域网就是多台电脑连接同一台交换机形成的网络(范围小),私有IP属于局域网。
现在我们的路由器大都继承了交换机功能,所以一般情况下连接同一个路由器的Lan端口或者无线网的设备就是在同一局域网下。
这个文章对你理解局域网与路由器有所帮助:LAN口和WAN口的区别是什么? - 知乎 (zhihu.com)
2.广域网(外网)
广域网是连接不同地区 局域网 或 城域网 计算机通信的远程网(范围大),公有IP属于广域网。
2.IP地址
IP地址就是访问到你设备的地址,通过这个地址,网络的另一端就能与你通讯交互,或者分享文件。按道理IP地址应该每个电脑都不一样,但是随着上网用户的增加,人们发现IP地址不够用了,所以发明了私有IP地址和公有IP地址。
公有IP地址: 全球唯一,不存在重复,由IP管理机构发放IP地址,任何公有IP地址之间可以直接通信。
私有IP地址:由路由器分配给连接的设备的IP地址,这些IP地址在同一个局域网下,他们之间可以直接通信。因为是路由器分配的地址,所以只有路由器才知道他们,所以不是该局域网的IP地址是不可以直接通信(可以间接通信,间接通信的方法在第二个解决方案)。
若想对IP进一步了解可以看这:什么是公有IP地址?什么是私有IP地址?。
3.NAT技术
NAT(Network Address Translation,网络地址转换)是1994年提出的。.当在 专用网(局域网) 内部的一些主机本来已经分配到了本地IP地址(即仅在本专用网内使用的专用地址(私有IP)),但现在又想和因特网上的主机通信(并不需要加密)时,可使用NAT方法。. 这种方法需要在专用网(私网IP)连接到因特网(公网IP)的路由器上安装NAT软件。. 装有NAT软件的路由器叫做NAT路由器,它至少有一个有效的外部全球IP地址(公网IP地址)。. 这样,所有使用本地地址(私网IP地址)的主机在和外界通信时,都要在NAT路由器上将其本地地址转换成全球IP地址,才能和因特网连接。.
2.原因分析
客户端Socket请求连接的IP地址是我电脑的IP地址,这是一个私有IP地址。而当时我女朋友在她的宿舍连着她宿舍的网,我在我宿舍连着我们宿舍的网,这样服务端和用户端就不在一个局域网下了,不在同一个局域网下的两个私有IP地址有这么可能直接访问呢?
3.解决方案
通过分析,我们的解决方法有多种
1.两台电脑在同一局域网(这不是我们想要的)
将我女朋友的电脑拿来和我一起连一个无线网,这样她的电脑就可以发现我的电脑的IP,然后就可以正常连接服务器了。
2.修改服务器主机路由器中的映射关系,将内网映射到外网(不是我们想要的)
该方法我没试过,因为我不知道我宿舍的路由器登录密码。。。
以下简单说原理:
上面介绍了NAT,如果你在百度搜IP,你会发现百度的IP地址和ipconfig出来的IP地址并不一样。
百度的IP地址才是你真正的上网用的IP地址也是路由器的公有IP地址。
修改路由器的映射关系,当别人访问路由器的公网IP的端口号时,路由器会自动映射到服务器主机的端口号上,这样也实现了我们的目的,但是要让别人修改用户端Socket的绑定IP为路由器的公有IP后运行代码。
如果每一次连接同一个路由器,路由器可能给我们分配不一样的IP地址,这是我们要去动态映射我们的服务器IP(我没接触过,但是看到过要用‘花生壳’这个软件你们可以自己研究)
如果服务端每次连接不同的路由器,我们甚至要改变客户端Socket绑定的IP,这就更麻烦了!
如果服务器端关闭,那么其他所有客户端的Socket都会断开连接,况且谁的电脑也不会一直开着,这样就会想到云服务器啊,它就会一直开着。
具体的修改映射关系的方法:如何将内网ip映射到外网_xiaonuo911teamo的博客-CSDN博客_将局域网ip映射到公网
3.云服务器
我用的是阿里的服务器,大家也可以用其他的华为啥的。
注册阿里云免费领云服务器_云服务器ECS_阿里云 (aliyun.com)
1.打开链接按照以下步骤可以免费领取云服务器,但免费的服务器只能领取一次
2.建议大家选择下面第二个,时间久一点,性能也够用
3.选择配置
地域:选择理你用户端近的,
带宽:5M
其他的不用改
点击购买,大家是第一次的话应该是0元。
4. 购买完成后进入云服务器管理控制台界面,并点击蓝色的实例名称
5.进入下面界面查看你的云服务器的信息(实例就是你申请的云服务器)
6. 点击重置实例密码修改密码(一定要)
7.点击安全组,添加入方向的规则
作用就是别人访问你服务器公网IP的端口时,可以映射到你服务器的私网IP的端口上(具体含义可以看上面的NAT技术)。
添加一个类似于红线上的规则,唯一要改的就是端口号,端口号范围至少要包含你服务器端的ServerSocket绑定的端口号,我的服务器端绑定的是7777,我就只写了7777。
端口范围可以自己选,范围为0 到65535。但不要和下面图片第二张图片中的一样,因为他们有固定的功能,即使你的云服务器上没有。
8.在云服务器运行服务器端代码
1.下载安装Xftp7和XShell7
XFTP - NetSarang Website (xshell.com)
Xftp7可以对你云服务器里的文件进行操作,并将文件传至云服务器。
Xshell7可以远程连接你的云服务器并执行命令。
2.打开Xftp7向云服务器发送你写的服务器端的java文件
在名称输入云服务器的公网IP后,主机会自动输入你输入的公网IP,然后点击连接.
输入用户名:root,记住用户名,然后点确定
输入密码(你修改的密码),记住密码,点确定就会连接到云服务器(如下图)
在右边的云服务器的里面创建一个java的文件夹,然后将左边你的电脑里的服务器端java文件拖到右边的java文件夹里(如下图)。(我的服务端用到了工具类所以两个java文件,拖进去之前将两个java文件里的包名删除(第一行package。。。),不然对下面运行代码有影响)
为什么会影响可以看一下下面的链接:
Java中的package及命令行下编译运行包下的java文件_yddcc的博客-CSDN博客_java运行包内文件
3.运行服务器里的服务器端java文件
打开Xshell7连接云服务器
新建会话,输入用户名和密码,和上面一样,不再赘述。
连接云服务器后会出现下面的界面(注意光标会变成root@。。。。。才表示成功)
输入 yum install java-1.8.0-openjdk-devel.x86_64 在云服务器下载jdk工具,jdk是用来运行java文件的。(这里就不放截图了,因为我已经安装过了,不行的可以上网搜做在云服务器下载jdk)
输入 cd /java cd命令是进入指定目录下(这里指进入java文件夹)
输入 javac *.java javac命令编译java文件成为class文件 ,*.java表示所有java文件(这里指编译该目录下的所有java文件),这时候你会发现java目录下会多出几个对应的class文件
输入nohup java 主类名& java命令是运行class文件(即运行代码),nohup &命令是不挂断进程的意思,使用是因为云服务器过十分钟左右会自动注销结束所有正在运行的进程。
这样云服务器就可以一直运行服务器代码了。
二、主要使用的类及方法介绍
1.ServerSocket类
此类实现服务器套接字。
1.public ServerSocket(int port,
int backlog,
InetAddress bindAddr)
使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。
参数:
port - 本地 TCP 端口
backlog - 侦听 backlog
bindAddr - 要将服务器绑定到的 InetAddress
2.public Socket accept()
阻塞方法,也就是说调用accept方法后程序会停下来等待连接请求
返回:
新套接字
3.public void close()
关闭此套接字。 在 accept() 中所有当前阻塞的线程都将会抛出 SocketException。
2.Socket类
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
1.public Socket(InetAddress address,
int port)
创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
参数:
address - 你要接入的服务器的IP 地址。
port - 端口号。
2.public InputStream getInputStream()
返回此套接字的输入流。
关闭返回的 InputStream 将关闭关联套接字。
返回:
从此套接字读取字节的输入流。
3.public OutputStream getOutputStream()
返回此套接字的输出流。
关闭返回的 OutputStream 将关闭关联套接字。
返回:
将字节写入此套接字的输出流。
4.public boolean isBound()
返回套接字的绑定状态。
返回:
如果将套接字成功地绑定到一个地址,则返回 true
5.public void close()
关闭此套接字。
关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream,进而关闭套接字。
3.InetAdress类
此类表示互联网协议 (IP) 地址。
1.public static InetAddress getLocalHost()
返回本地主机。
返回:
本地主机的 IP 地址。
二、程序流程简介
上图可以这样理解:
你在家想找外地人讲话,很明显不行,那么现在你找到了一家公司,这个公司说可以找个人专门帮你传话,你要将你的话告诉传话人,而传话人会将你说的话告诉公司里的所有的传话人,这样其他传话人就会告诉他服务的人。别人说的话也会通过传话人告诉你。
你就是用户端的Socket
公司就是ServerSocket
公司找的专门帮你传话的人就是accept()返回的服务Socket
用户端的线程:让用户可以一直向传话人传达自己消息和接收传话人向自己传递的消息
服务器端的服务线程:让这个传话人一直为你服务(向别人传递你的信息和向你传递别人的消息)
三、具体代码
1.服务器端
package com.csi.qunliaoTest;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class OpenServer {
//一个列表存储服务Socket
List<ClientSocket> clientSockets =new ArrayList<>();
ServerSocket serverSocket = null;
//构造方法中创建服务器ServerSocket
public OpenServer() {
try {
//因为本代码是在云服务器运行,直接用InetAddress.getLocalHost()方法绑定云服务器的IP地址及端口号
serverSocket = new ServerSocket(7777, 50, InetAddress.getLocalHost());
System.out.println("----------服务器----------");
} catch (IOException e) {
//发生异常。调用自己写的Utils类关闭服务器
Utils.close(serverSocket);
}
//ServerSocket绑定成功,开始等待用户接入
if(serverSocket.isBound())
acceptClient();
}
//等待用户,并为他生成服务Socket
private void acceptClient(){
//while循环让服务器一直可以接入用户,一个用户接入服务器,服务器就生成一个为该用户服务的Socket
while(true) {
Socket socket=null;
try {
//ServerScoket的accept()方法是一个阻塞方法,他会在这里等用户接入,直到有用户接入,才会运行下面的代码
socket=serverSocket.accept();
System.out.println("一个用户接入.....");
//用ClientSocket类包装Scoket类,ClientSocket类是自己写的内部线程类,该类实现了接收和转发用户消息
ClientSocket clientSocket=new ClientSocket(socket);
//开启服务线程
clientSocket.start();
//将这个包装了服务Scoket的对象添加进列表
clientSockets.add(clientSocket);
} catch (IOException e) {
Utils.close(socket);
}
}
}
public static void main(String[] args) {
//执行服务器代码
new OpenServer();
}
class ClientSocket extends Thread{
Socket socket=null;
DataInputStream dataInputStream=null;
DataOutputStream dataOutputStream=null;
public ClientSocket(Socket socket) {
this.socket = socket;
//包装服务Socket的输入输出流,异常就调用closeScoket()方法
try {
dataInputStream=new DataInputStream(socket.getInputStream());
} catch (IOException e) {
closeSocket(dataInputStream,socket);
}
try {
dataOutputStream=new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
closeSocket(dataOutputStream,socket);
}
}
//上面的代码我们关闭的都是装饰流,因为关闭装饰流会将内部流也关闭,Socket也会因此关闭,同时我们也要将列表里对应Socket的删除
public void closeSocket(Closeable...closeables) {
Utils.close(closeables);
System.out.println("一位用户退出");
clientSockets.remove(this);
}
@Override
public void run() {
//读取用户姓名,并让列表所有用户转发欢迎信息
//数据流DataIn/OutputStream的readUTF()和writeUTF(String data)要一起用,是将数据以UTF-8的编码方式发出或者接收
String name = null;
try {
//readUTF()方法也是阻塞方法,读取用户发来的名字
name=dataInputStream.readUTF();
} catch (IOException e1) {
closeSocket(dataInputStream);
}
//循环列表,除了自己,其他服务Scoket全部转发消息
for(ClientSocket clientSocket:clientSockets) {
if(clientSocket!=this) {
try {
clientSocket.dataOutputStream.writeUTF("欢迎"+name+"进入聊天室");
clientSocket.dataOutputStream.flush();//清空缓存区,让缓存区的数据全部出来
} catch (IOException e) {
clientSocket.closeSocket(clientSocket.dataOutputStream);
}
}
}
//知道名字后就一直等待接收用户端发的消息,异常就关闭Scoket并跳出循环
while(true) {
String msg = null;
//在读取到的消息前加上姓名
try {
msg = name+":"+dataInputStream.readUTF();
} catch (IOException e) {
closeSocket(dataInputStream);
break;
}
for(ClientSocket clientSocket:clientSockets) {
if(clientSocket!=this) {
try {
clientSocket.dataOutputStream.writeUTF(msg);
clientSocket.dataOutputStream.flush();
} catch (IOException e) {
clientSocket.closeSocket(clientSocket.dataOutputStream);
}
}
}
}
}
}
}
2.用户端
使用客户端代码时要改里面Socket要绑定的IP为云服务器的公网IP哦
package com.csi.qunliaoTest;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
public class OpenClient {
String name=null;
Socket socket=null;
Scanner scanner=null;
public OpenClient() {System.out.println("请输入你的姓名");
scanner=new Scanner(System.in);
name=scanner.nextLine();
try {
//请求连接云服务器,这里的IP要写云服务器的公网地址和你在安全组创建的端口号,经过路由器的地址映射,就会请求到云服务器的私网IP的接口上。
socket = new Socket("192.168.10.115",7777);
//如果成功链接云服务器,就开启该Socket的发送和接收消息线程
if(socket.isBound()) {
new SendMsgThread().start();
new ReceiveMsgThread().start();
}
} catch (UnknownHostException e) {
Utils.close(socket,scanner);
} catch (IOException e) {
Utils.close(socket,scanner);
}
}
public static void main(String[] args) {
new OpenClient();
}
//接收消息线程
class ReceiveMsgThread extends Thread{
DataInputStream dataInputStream=null;
public ReceiveMsgThread() {
try {
dataInputStream=new DataInputStream(socket.getInputStream());
} catch (IOException e) {
Utils.close(dataInputStream);
}
}
@Override
public void run() {
while(true) {
try {
System.out.println(dataInputStream.readUTF());
} catch (IOException e) {
Utils.close(dataInputStream);
break;
}
}
}
}
//发送消息线程
class SendMsgThread extends Thread{
DataOutputStream dataOutputStream=null;
public SendMsgThread() {
try {
dataOutputStream=new DataOutputStream(socket.getOutputStream());
} catch (IOException e) {
Utils.close(dataOutputStream);
}
}
@Override
public void run() {
//先将名字发送服务器
try {
dataOutputStream.writeUTF(name);
dataOutputStream.flush();
} catch (IOException e) {
Utils.close(socket);
}
String msgString;
//读你在控制台输入的信息并判断是不是bye,是bye就关闭Socket和Scanner,不是就转发
while(true) {
if(!(msgString=scanner.nextLine()).equals("bye")) {
try {
dataOutputStream.writeUTF(msgString);
dataOutputStream.flush();
} catch (IOException e) {
Utils.close(socket,scanner);
}
}
else {
Utils.close(socket,scanner);
break;
}
}
}
}
}
3.工具类
package com.csi.qunliaoTest;
import java.io.Closeable;
import java.io.IOException;
public class Utils {
//Closeable是一个接口实现了所有的流类,Closeable 是可以关闭的数据源或目标。调用 close 方法可释放对象保存的资源(如打开文件)。
public static void close(Closeable...closeables) {
for(Closeable closeable:closeables) {
if(closeable!=null)
try {
closeable.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
四、运行结果
如下是我在我的电脑上运行的两个用户(云服务器一直在运行我的服务端代码)