一、Socket编程
(一)什么是Socket编程
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。但是,Socket所支持的协议种类也不光TCP/IP一种,还有一种UDP协议。Java编程一般使用TCP/IP协议。
(二)Socket通讯模式
Server端Listen(监听)某个端口是否有连接请求,Client端向Server 端发出Connect(连接)请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client 端都可以通过多种方法与对方通信。
对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤:
(1) 创建Socket;
(2) 打开连接到Socket的输入/出流;
(3) 按照一定的协议对Socket进行读/写操作;
(4) 关闭Socket.
实战
项目要求:设计一个多人聊天程序,可以实现群聊和私聊功能。
设计思路:
第一步:由于客户端只能向服务器端发送文件或者字符,服务器端只能得到客户端发过来的文件和字符,所以客户端和服务器端之间数据传递需要有一个有一个规则以便客户端的需求可以在发送的字符里面体现。因此我们做出以下规范:
登陆: "u+用户名u+" 比如:u+Jacku+ ,这个字符串中"u+"表示登录,"Jack"是用于登录的用户名
返回结果: 成功1 失败-1
私聊: "p+消息接收者的用户名♥聊天内容p+" 比如:"p+Jack♥hellop+"表示,消息发给Jack,消息的内容是hello;
-
群聊: " a+消息内容 a+"
由于我们要定义一套规范,所以要使用接口来实现:public interface ChatProtocol { //登录 String LOGIN_FLAG = "u+"; //私聊 String PRIVATE_FLAG = "p+"; //群聊 String PUBLIC_FLAG = "a+"; //分隔符 String SPLIT_FLAg = "♥"; //成功状态 String SUCCESS = "1"; //失败状态 String FALSE = "-1"; }
第二步:我们可以先写服务器端,先创建一个serverSocket模拟服务器,接着就要循环监听连接到服务器的所有客户端,并把这些客户端(用户)存储起来。存储用户可以使用Map集合,可以把用户名和客户端的socket一一对应存储起来。由于用户比较多,可以定义一个用户管理器来管理这些用户。由于服务器端不但要接收来自各个客户端上传的数据,还要给每个客户端发送数据,因此需要用到多线程来处理对每个客户端的数据接收和发送。具体实现如下:
public class Server {
//用户管理器,用于管理每个用户对应的名字和socket
public static UserManager userManager = new UserManager(); //单例
public static void main(String[] args) {
//创建serverSocket
// 此种异常处理的好处是:如果出现异常,直接关闭serverSocket,不需要再写finally模块
try( ServerSocket serverSocket = new ServerSocket(8888)) {
//监听所有来连接的客户端
while(true){
Socket clientSocket = serverSocket.accept();
//让子线程处理这个clientSocket
new ServerThread(clientSocket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端代码结构:
- 1,先创建一个serverSocket;
- 2,循环监听哪个客户端连接到服务器,并得到这个客户端的socket
- 3,得到客户端的socket后,交由子线程来对这个客户端进行处理,子线程的具体代码如下:
class ServerThread extends Thread{
private Socket socket;
public ServerThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
//登录
//1,获取输入流对象
BufferedReader bufferedReader = null;
//创建服务器端输出流对象
PrintStream printStream = null;
try {
//接收客户端输入流
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//向客户端输出数据的输出流
printStream = new PrintStream(socket.getOutputStream());
String line = null;
while((line = bufferedReader.readLine()) != null){ //读取客户端输入的数据,即用户名
//获取客户端传输的数据是否是请求登录以及用户名
if(line.startsWith(ChatProtocol.LOGIN_FLAG) && line.endsWith(ChatProtocol
.LOGIN_FLAG)){
//如果是登录请求,就将数据里面的用户名提取出来
String name = line.substring(2,line.length()-2);
//判断提取出的用户是否已经登录过
if(Server.userManager.isLogined(name)){
//登录过了 发送给客户端失败状态
printStream.println(ChatProtocol.FALSE);
}else{
//没有登录,发送给客户端登录成功
// 并保存当前登录的用户信息
Server.userManager.save(name,socket);
printStream.println(ChatProtocol.SUCCESS);
}
}
//判断客户端发送的数据是不是发起私聊的请求
else if(line.endsWith(ChatProtocol.PRIVATE_FLAG)&& line.startsWith(ChatProtocol.PRIVATE_FLAG)){
//如果是私聊请求,就先把请求标志"p+"从数据里去除
String message = line.substring(2,line.length()-2);
//然后以"♥"为分隔符,把消息要发送给的目标用户的用户名,和发送的消息内容分隔开
String[] items = message.split(ChatProtocol.SPLIT_FLAg);
//得到用户名
String name = items[0];
//得到需要发送消息的内容
String content = items[1];
//通过用户名找到对应的客户端的socket
Socket desSocket = Server.userManager.findSocketByname(name);
//创建服务器端对这个目标用户的输出流
PrintStream desps = new PrintStream(desSocket.getOutputStream());
//获取当前用户(消息发送者)的名称
String currentName = Server.userManager.nameBySocket(socket);
//将消息内容以及消息发送送者的用户名发送给目标用户
desps.println(currentName+"向你发来消息:"+content);
}else{
//最后剩下的可能就是群聊请求
//提取客户端输入数据中的群聊消息内容
String message = line.substring(2,line.length()-2);
//获取当前用户(群聊消息发送者)的名称
String currentName = Server.userManager.nameBySocket(socket);
//获取所有用户的socket,以便给每个用户发送消息
Collection sockets = Server.userManager.allUsers();
//给每个用户发送群聊的消息
for(Socket socket1:sockets){
//创建对应每个用户的服务器输出流
PrintStream temps = new PrintStream(socket1.getOutputStream());
// 将群聊消息发送给每个用户
temps.println(currentName+"发来群聊消息:"+message);
//temps.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
super.run();
}
}
服务器端子线程代码结构:
- 1,创建一个socket对象,主线程由构造方法将需要处理的客户端(用户)传给子线程
- 2,创建客户端的输入流对象BufferedReader,用于接收客户端的输入的数据
- 3,创建服务器端输出流对象PrintStream,用于给客户端发送数据
- 4,将客户端发送的数据读取出来,进行判断筛选,主要是判断字符串的开头和结尾:
- 以"a+"开头和结尾:
1,先提取字符串中的用户名;
2,判断是否登录过;
3,将判断结果发送给客户端;- 以"p+"开头和结尾:
1,先提取出消息接收者的用户名和消息内容;
2,把消息接收者和消息内容分割开;
3,将消息发送给消息接收者的客户端;- 剩下的是群发消息:
1,提取出消息内容;
2,获取所有用户的客户端socket;
3,获取消息发送者的客户端;
4,将消息发送者的用户名和消息内容发送给每个客户端;
(更具体请看代码注释)
注:各种方法都在用户管理器中,用户管理器的代码结构就不再详细介绍,具体请看注释,代码如下:
public class UserManager {
//保存所有用户信息
private Map users = new HashMap<>();
//判断用户是否已经登陆
public boolean isLogined(String name) {
//遍历数组
for (String key : users.keySet()) {
if (key.equals(name)) {
return true; //已经登录
}
}
return false;
}
/**
* 保存当前登录的用户信息
* @param name
* @param socket
*/
public void save(String name,Socket socket){
users.put(name,socket);
}
/**
* 通过用户名找到对应的socket
* @param name
* @return
*/
public Socket findSocketByname(String name){
return users.get(name);
}
/**
* 通过socket对象找到对应的名称
* @param socket
* @return
*/
public String nameBySocket(Socket socket){
for(String key:users.keySet()){
//取出这个key对应的socket
if(socket == users.get(key)){
return key;
}
}
return null;
}
/**
* 获取所有人的socket
* @return
*/
public Collection allUsers(){
return users.values();
}
}
第三步:我们要写一个客户端,接着要告诉这个客户端需要连接的服务器的IP地址和端口号,然后就是要用户登录,用户从终端输入,为了提示用户输入,这里使用了一个小界面,然后在把用户输入的用户名按照之前的规范拼接成合格的字符串发送给服务器;由于客户端不但要从终端输入数据,并发送给服务器端,还要及时接收服务器端发送来的数据,因此需要用到子线程来处理服务器端输送过来的数据。
public class Client {
public static void main(String[] args) {
BufferedReader bufferedReader = null;
PrintStream printStream = null;
//连接服务器
try ( Socket socket = new Socket("10.129.25.52",8888)){
//发给服务器的输出流
printStream = new PrintStream(socket.getOutputStream());
//接收服务器的输入流
BufferedReader bufferedReaderServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//接收终端的输入流
bufferedReader = new BufferedReader(new InputStreamReader(System.in));
//登录
while(true){
//提示用户输入用户名
String line = JOptionPane.showInputDialog("请输入用户名");
//拼接登录格式
String loginSreing = ChatProtocol.LOGIN_FLAG+line+ChatProtocol.LOGIN_FLAG;
//将拼接后的请求登录数据发送给服务器
printStream.println(loginSreing);
//接收服务器返回的是否登录成功的结果
String result = bufferedReaderServer.readLine();
//判断接收的登录结果
if(result.equals(ChatProtocol.SUCCESS)){
//登陆成功
System.out.println("登录成功");
break;
}else{
//用户名重复 登录失败
System.out.println("用户名已存在,请重新登录");
}
}
//登录成功在这里执行
// 开启子线程处理服务器端的输入过来的数据
new ClientThrend(socket).start();
//将终端输入的数据发送给服务器端
String line = null;
while((line = bufferedReader.readLine()) != null){
//发送给服务器
printStream.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端代码结构:
- 1,创建socket,并将IP地址和端口号传递给服务器;
- 2,创建客户端输出流,用于将数据传送给服务器;
- 3,创建终端输入流,用于接收终端输入;
- 4,创建服务器端输入流,用于接收服务器端输入;
- 5,拼接终端输入的用户名,并发送给服务器;
- 6,接收服务器端对用户名的判断结果;
- 7,如果登录成功,读取其他终端输入的数据,并发送给服务器端;
- 8,创建子线程来处理服务器端发送的数据,子线程代码如下:
(更具体请看注释)
class ClientThrend extends Thread{
Socket socket;
//记录处理哪个客户端
public ClientThrend(Socket socket){
this.socket = socket;
}
//创建服务器端输入流
BufferedReader bufferedReader = null;
@Override
public void run() {
try {
//服务器端输入流 接收服务器发来的数据
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
//读取服务器端发来的数据
while((line = bufferedReader.readLine()) != null){
System.out.println(line); //打印服务器端发来的数据
}
} catch (IOException e) {
System.out.println("网络异常");
}finally{
//一旦出现异常,就关闭相关资源以及socket
try {
if(bufferedReader != null) {
bufferedReader.close();
}
if(socket != null){
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
super.run();
}
}
子线程代码结构:
- 1,创建一个scoket对象,由主线程从构造方法将需要处理哪个客户端的数据告诉子线程;
- 2,创建服务器端输入流对象,接收服务器端传来的数据;
- 3,读取服务器端的数据,并打印出来;
- 4,出现异常时,关闭相关资源以及socket;
(更详细请看注释)
学习感悟
今天写到项目感觉非常有趣,写起来也很有兴致。但是这个项目稍微有点难度,所以在跟着老师写完之后我又看了几遍,也不是很懂,然后我就几乎把每一行代码都写上注释,这样在写注释的时候思路就变得清晰起来,所以我感觉这时一种不错的学习办法。