套接字及客户服务器应用程序基础
本次实验要求自己动手实现一个能够在局域网中进行点对点聊天的实用程序。
本人采用面向对象的Java编程语言,学习了基于对话框的windows应用程序的编写过程,实现TCP套接字编程。
PC、Windows操作系统、Eclipse
计算机网络就是指将地理位置不同的计算机通过通信线路连接起来,实现资源共享和信息传递。网络编程就是通过程序实现两台(或多台)主机之间的数据通信。 要想实现这一目标,首先要建立连接,然后按照事先规定好的格式进行数据传输,从而完成主机之间的信息传输。
(1) IP地址和域名
IP地址是识别网络主机的唯一身份标识。IP地址为32位二进制数(IPv4) ,通常写成4个0~255之间的数字,即点分十进制形式。
域名可看作IP地址的别称,用字符描述,便于理解和记忆。在网络通信过程中只能使用IP地址。 DNS是地址解析系统,实现域名与IP地址的转换工作。
(2) 端口号和Socket
端口号port是用于识别主机各进程的标识。通过目标主机IP地址+目的端口号 即可定位到 某个主机的某个进程。网络编程也称为基于Socket的编程。
计算机网络中规定端口号由16位二进制数表示,即十进制数范围为0~65535。 0~1023端口为周知端口,被系统进程或常用服务占用,所以选择端口时要选择1024以后的端口。根据连接端的不同,socket可分为客户端套接字和服务器端套接字。java.net.Socket类 和 java.net.ServerSocket类 用于客户机与服务器端的网络编程。
(3) C/S模式
C/S模式:客户端/服务器端模式,简称C/S模式。
Client是发起请求的一方,被称作客户端;
Server是接受请求的一方,被称作服务器端。
B/S模式:特殊的C/S模式。
Browser是浏览器,不需要安装额外的客户端程序就能访问服务器端。
(4) 线程池
这个内容比较多,参考搜到的相关知识,搞了篇总结:
线程和线程池_找不到我吧我独一无二的博客-CSDN博客
一种面向连接的可靠的传输协议。采用通信双方相互应答的方式来保证数据传送的可靠性。网络的通信开销增加,协议也更为复杂。大部分网络通信都采用TCP
协议。
一种面向无连接的传输协议。不需要通信双方事先建立连接和应答就进行传输。协议简单,效率更高,但不保证通信的可靠性。适用一些简单的网络应用。
TCP连接为客户机和服务器提供了一个直接的传输管道,进行可靠的、顺序的、字节流的传输。
客户端是发起连接请求的程序。首先建立网络连接,在建立连接时需要指出服务器端的IP地址和端口号。 连接建立成功,就可以实现数据交互。数据交互时按照请求-响应模型由客户端向服务器端发送请求,服务器端根据请求内容进行处理,并将响应结果返回给客户端。 数据的交互过程可以进行多次,每次均按照请求-响应模型进行。 在数据交互结束后,关闭网络连接,释放占用的端口、内存等资源,结束客户端程序。
服务器端程序监听固定的端口。当服务器端监听到客户端的连接请求后,与客户端建立一个网络连接。连接建立成功后双方进行数据交互。数据交互结束,关闭服务器端,释放占用的资源。在实际编程中,为解决多用户响应问题,通常采用多线程机制。
采用tcp连接,多个客户端进行即时通信,使用一个服务器作为中介,转发客户端之间的交流信息,ip地址均为本机回环地址即localhost(127.0.0.1),然后使用端口号进行客户端寻址,使用 hashmap存储端口号和客户端服务器的tcp套接字的映射。
客户端1 <-- tcp长连接 --> 服务器
客户端2 <-- tcp长连接 --> 服务器4
……
客户端x <-- tcp长连接 --> 服务器
服务器与多个客户端之间都建立了tcp连接,且保存了每一个的socket对象和客户端端口的映射,所以服务器可以找到每一个客户端。
客户端x发消息
服务器接收然后通过消息头部的端口号找到目的客户端
转发给客户端y,完成即时通信
画了一个客户机/服务器程序交互图:
其中通过套接字建立连接的关键点就在于 – accept()函数 :
服务器端建立欢迎套接字后调用accpet()等待连接请求,直到客户端请求建立连接,服务器accept客户端的连接请求,并返回一个套接字,客户机通过此套接字与服务器通信。如果未连接到客户端,线程处于阻塞状态,程序不执行下去。
一个服务器可以接受多个客户端的连接请求,但其只为第一个已连接套接字服务,只与第一个客户端通信,不会与其他的客户端交互。如果要为多个客户端服务,可让服务器接收的客户端请求(Socket socket=serverSocket.accept())处于循环中,就相当于有n个服务器,这样就可以与n个客户端通信。
客户端点击“绑定端口 建立连接时”,线程在成功与客户端建立连接之后指向此函数,将分配的端口号返回给客户端,并且将端口号和套接字一同存放到map中,
相当于每当用户建立连接后,将其进行注册,以便之后服务器能找到该用户。
private void userRegist(String userName,Socket socket) throws IOException{
//返回socket 值
PrintStream printStream=new PrintStream(socket.getOutputStream());
String port=socket.toString().split(",")[1].split("=")[1];
printStream.println(port);
map.put(port,socket); //将 [端口号,套接字] 存一份
System.out.println("[用户名为"+userName+"][客户端为"+socket+"]上线了!");
System.out.println("当前在线人数为:"+map.size()+"人");
}
接收到页面传来的消息,对其进行解封,得到端口号、消息、用户昵称,然后在map中用get方法根据端口号找到对应的socket套接字,然后将消息封装了当前时间后将其发送给目标客户端。
/**
* 私聊流程(利用port取得目标客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
* @param port 目标客户端端口号
* @param userName 发送方的用户名
* @param msg 要发送的信息
*/
private void privateChat(String port,String userName,String msg) throws IOException {
//1.当前发送方客户端的用户名
String curUser=userName;
//2.取得私聊用户名对应的客户端
Socket client=map.get(port);
//3.获取私聊客户端的输出流,将私聊信息发送到指定客户端
PrintStream printStream=new PrintStream(client.getOutputStream());
Date = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); //获得当前时间
String m = formatter.format(date)+"@"+curUser+":"+msg;
System.out.println(formatter.format(date));
System.out.println(m);
printStream.println(m);
//socket.close();
}
包含一个与当前客户端通信的套接字,并重写了run方法,定义了服务器端接收到客户端输入后的操作,其中:
接收到的客户端的消息,可能是用户登录(建立连接)的信息,或发送的聊天信息,
可根据发送信息的格式判断为哪种情况并采取相应操作。
//其中 userRegist与privateChat 前面已介绍过
class Server implements Runnable{
private static Map<String,Socket> map=new ConcurrentHashMap<>();
private Socket; // 与客户端通信的套接字
public Server(Socket socket){
this.socket=socket;
}
@Override
public void run() {
// 获取客户端的输入流
try {
Scanner scanner=new Scanner(socket.getInputStream());
String msg=null;
while(true){
if(scanner.hasNextLine()){
//处理客户端输入的字符串
msg=scanner.next();
System.out.println("服务器接收:"+msg);
//用户登录流程,注册用户的格式为:userName:用户名
if(msg.startsWith("userName:")){
//将用户名或者端口号保存在userName中
String userName=msg.split("\\:")[1];
//注册该用户
userRegist(userName,socket);
continue;
}
//聊天信息,输入的格式为:P:port-userName-聊天信息
else if(msg.startsWith("P:")&&msg.contains("-")){
//保存需要聊天的目标端口、发送者昵称、发送的信息
String port=msg.split("\\:")[1].split("-")[0];
String name = msg.split("\\:")[1].split("-")[1];
String str=msg.split("\\:")[1].split("-")[2];
//发送聊天信息
privateChat(port,name,str);
continue;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//登录 建立连接
private void userRegist(String userName,Socket socket) throws IOException { }
//流程(利用port获取目标客户端的Socket对象,从而取得对应输出流,将私聊信息发送到指定客户端)
private void privateChat(String port,String userName,String msg) throws IOException{ }
}
建立线程池,每与一个客户端建立tcp连接之后在线程池中添加一个线程。
public static void main(String[] args){
try {
//1.创建服务器端的ServerSocket对象,等待客户端连接
ServerSocket serverSocket=new ServerSocket(9527);
//2.创建线程池,从而可以处理多个客户端
ExecutorService executorService= Executors.newFixedThreadPool(50);
for(int i=0;i<50;i++){
System.out.println("欢迎来到我的聊天室......");
//3.侦听客户端
Socket socket=serverSocket.accept();
System.out.println("有新的朋友加入.....");
//4.启动线程
executorService.execute(new Server(socket));
}
//5.关闭线程池
executorService.shutdown();
//6.关闭服务器
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
用户界面部分的代码使用了swing包进行编程,有目的ip,目的端口,本人昵称,本机端口,绑定按钮,显示文本域,发送文本域,发送按钮等组件。
在main函数中构造页面并声明一个客户端处理类的对象。
其中back函数用来显示服务器返回消息(另一个客户端处理类中使用)。
// 这个代码太长啦,我把所有代码放到另一篇csdn上了,后面有完整代码的链接
主要部分:
public static void back(String m) {
backMeg = m;
if(backMeg.contains(":")) {
String context = backMeg;
String name ="\n"+ backMeg.split("@")[1].split("\\:")[0] + ":";
context=context.replace('@','\n'); //时间隔开
context=context.replace("$",name);
area.append(context+"\n"); //显示文本内容增加
}
}
JButton btnBind = new JButton("绑定端口 建立连接");
btnBind.setBackground(new Color(255,221,221));
btnBind.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
try {
client.mainClient();
//登录
client.send("userName:"+jtName.getText());
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//暂停进程 等待服务器返回数据
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println("***"+backMeg);
//拿到绑定的本机端口号
jtmyPo.setText(backMeg);
}
});
//发送按钮
JButton btn = new JButton("发送");
btn.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//String sendName = jtName.getText().equals("")?"I AM NULL":jtName.getText();
String context = jt.getText().equals("")?"空空如也":jt.getText();
context=context.replace('\n','$');
jt.setText(""); //清空输入
jt.requestFocus(); //获取光标
String sendPort = desJtPo.getText(); //获取发送端口
if(sendPort.equals("")) return;
String name = jtName.getText();
String sendMes = "P:" + sendPort +"-"+ name + "-" + context;
System.out.println("发送框中的内容是:"+sendMes);
client.send(sendMes);
//暂停进程 等待服务器返回数据
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
//System.out.println("发送后接收到的内容是:"+backMeg);
//area.append(backMeg+"\n"); //文本内容增加
}
});
btn.setBounds(490, 490, 70, 40);
btn.setBackground(new Color(255,221,221));
btn.setFont(f1);
c.add(btn);
读线程,用套接字做scanner的扫描器,当服务器有返回值时进入while循环,并调用Frame类即界面类的静态函数 改写界面其中的值并渲染显示。这里有两种返回值,一是套接字建立后服务器返回的端口号,需要将其显示出来用于连接对话,二是返回的另一个客户端发送过来的消息,需要将其附加到显示文本域中。
class ClientReadServer implements Runnable{
private Socket socket;
public ClientReadServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
// 获取服务器端输入流
try {
Scanner scanner=new Scanner(socket.getInputStream());
System.out.println("test");
while(scanner.hasNext()){
// System.out.println("内容:"+scanner.nextLine());
String meg=scanner.nextLine();
System.out.println("服务端返回:"+meg);
Frame.back(meg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
写线程。while永真循环将线程卡在此忙等,当sendFlag标记为真时,这表示有东西要写入发送给服务器,就是发消息了。而change静态函数会被客户端处理类中send函数调用,使得标记位变为真,且消息刷新,完成发送消息的动作。
class ClientSendServer implements Runnable{
private static boolean sendFlag = false; //发送标记 点击发送后变true
private static String msg;
private Socket socket;
public ClientSendServer(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
// 获取服务器端的输出流
PrintStream printStream=new PrintStream(socket.getOutputStream());
while(true){
if(sendFlag){
printStream.println(msg);
System.out.println("线程中run 发送:"+msg);
sendFlag = false; //发送完成后置为false
}
//暂停进程
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// System.out.println("进程进行中!");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void change(String m,boolean b) {
msg = m;
sendFlag = true;
}
}
客户端连接服务器端,服务器通过accept()接受连接并返回套接字Socket对象;
然后创建读取服务器端信息的线程和发送服务器端信息的线程并启动。
其中Send函数会在界面端点击发送按钮后调用。
public class Client {
public void mainClient() throws IOException{
//1.客户端连接服务器端,返回套接字Socket对象
Socket socket=new Socket("127.0.0.1",9527);
//2.创建读取信息的线程和发送信息的线程
Thread read=new Thread(new ClientReadServer(socket));
Thread send=new Thread(new ClientSendServer(socket));
//3.启动线程
read.start();
send.start();
}
public void send(String mes) {
//发送信息
System.out.println("准备发送的数据:"+mes);
ClientSendServer.change(mes, true);
}
}
[计算机网络实验4 - TCP套接字编程 - 点对点聊天 - 代码实现] - 找不到我吧我独一无二的博客 - CSDN博客