完整版代码
socket 是java中的一插口,要实现网络通信的话,需要连接插口和插口,而数据的传输使用了流的思想,读数据操作运用了输入流,而写数据运用了输出流,
聊天对话的实现
一个类作为服务端对象,用于接受客户端写出的输出流,
一个类作为客户端对象,用于输到的服务端的进行读取的输入流.
client客户端的类
私有一个成员变量 Socket插口
构造方法会优先比普通方法先调用,这边可以先定义一下客户端新建一个实例对象初始化的行为
public class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,使用它可以和远端计算机建立网络链接,并基于
两条流(一条输入,一条输出)的读写与对方进行数据交换。
*/
private Socket socket;
/**
* 构造器,用于初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
Socket实例化时就是与服务端建立链接的过程,此时需要传入两个
参数
参数1:服务端的IP地址,用于找到服务端的计算机
参数2:服务端口,用于找到服务端程序
如何查看IP地址:
windows:窗口键+R 打开控制台
输入ipconfig
查看以太网适配器-以太网,找到ipv4查看自己的IP地址
mac:打开[终端]程序
输入/sbin/ifconfig查看自己的IP地址
*/
// socket = new Socket("127.0.0.1",8088);//127.0.0.1和localhost都是表示本机
socket = new Socket("localhost",8088);
System.out.println("与服务端成功链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
主要点 Socket 需要传递两个参数 ip地址(服务端) 服务端占用的端口
ip地址 查看
如何查看IP地址:
windows:窗口键+R 打开控制台
输入ipconfig
查看以太网适配器-以太网,找到ipv4查看自己的IP地址mac:打开[终端]程序
输入/sbin/ifconfig查看自己的IP地址
*/
socket端口查看
Windows系统:
- 按住Windows键+R,唤出运行对话框。
- 在对话框里输入“cmd”,然后点确定,就会出现cmd窗口。
- 在cmd窗口中输入命令“netstat -an”,然后回车,就能看见当前连接的列表,其中冒号后面跟着的数字就是端口号
这边定义一个运行方法
public void start(){
/*
Socket提供的方法:
OutputStream getOutputStream()
通过Socket获取一个字节输出流,通过向该流写出字节,就可以发送给远端链接
的计算机的Socket了
*/
try {
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
pw.write(12);}catch (IOException e) {
e.printStackTrace();
} finally {
try {
//socket的close方法会进行四次挥手
//并且也会关闭通过socket获取的输入流和输出流
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
finally是无论程序执行过程中是否发生异常都会执行,并且在强制退出方法也会执行,使用return中断下方的代码,强制退出该方法也会执行finally,
选择拔插口是不选择关闭文件流是为了告诉服务端 ,客户端想要中断输出,是为了保证传输的可靠性
三次握手主要用于建立TCP连接。这个过程包括三个步骤:
- 客户端发送一个带有SYN(synchronize)标志的数据包给服务端,并进入SYN_SEND状态,等待服务器确认。
- 服务端收到SYN包后,确认客户的SYN(ACK=客户端序列号+1),同时自己也发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态。
- 客户端收到服务器的SYN+ACK包后,向服务器发送确认包ACK(ACK=服务器序列号+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。完成三次握手后,客户端与服务器端开始传送数据。
四次挥手则用于断开TCP连接。这个过程同样包含四个步骤:
- 客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT1状态。
- 服务端收到连接释放报文,发出确认报文,ACK=1,ack=序号+1,并且带上自己的序列号,服务端就进入了CLOSE_WAIT状态。此时TCP连接处于半关闭状态,即客户端已经没有要发送的数据了,但服务端若发送数据,则客户端仍要接受。
- 若服务端也没有数据要发送了,就通知客户端,发送一个FIN,即释放数据报文,之后服务端进入LAST_ACK状态,等待客户端的确认。
- 客户端收到服务端的释放数据报文后,必须发出确认,ACK=1,ack=序号+1,而自己的序列号是seq=序号+1,此时,客户端就进入了TIME_WAIT状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
四次挥手中,服务器发送完ACK之后,释放了服务器端到客户端的数据传送,但是客户端到服务器端的这个方向数据传送还没有释放,所以客户端最后还要发送一次确认。
总的来说,三次握手和四次挥手确保了TCP连接的建立和断开过程中的数据同步和可靠性,是TCP/IP协议中非常重要的部分。
最后运行
public static void main(String[] args) { //实际开发中不会在main方法中写业务逻辑,main方法是静态方法会有很多不便 Client client = new Client();//调用构造器初始化客户端 client.start();//调用start方法使客户端开始工作 }
服务端的类
服务端构造方法定义一个启动服务端使用初始化行为,私有化的服务端插口
public class Server {
/*
java.net.ServerSocket
运行在服务端的ServerSocket相当于时客户中心的"总机",上面有若干的插座(Socket)
客户端的插座就是与总机建立链接,然后总机这边分配一个插座与之建立链接,来保持双方
通讯的。
ServerSocket有两个主要工作
1:创建时向系统申请服务端口,以便客户端可以通过端口找到
2:监听该端口,一旦一个客户端链接,便创建一个Socket,通过它与客户端通讯
*/
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端");
/*
实例化ServerSocket时需要指定向系统申请的服务端口,如果该端口
已经被系统的其他应用程序占据,则这里会抛出异常
java.net.BindException: Address already in use: bind()
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
ServerSocket 的参数是一个服务端占用的端口
服务端运行的方法
public void start(){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket的重要方法:
Socket accept()
该方法是一个阻塞方法,调用该方法后程序会"卡住",直到一个客户端使用
Socket与服务端建立链接为止,此时accept方法会立即返回一个Socket
通过返回的Socket就可以与链接的客户端双向通讯了。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String message;
while ((message = br.readLine()) != null) {
//张三[192.168.2.5]说:XXXX
System.out.println(nickname+"[" + ip + "]说:" + message);
} catch (IOException e) {
e.printStackTrace();
}
}
服务端运行程序
public static void main(String[] args) { Server server = new Server(); server.start(); }
整理
客户端
ublic class Client {
/*
java.net.Socket 套接字
Socket封装了TCP协议的通讯细节,使用它可以和远端计算机建立网络链接,并基于
两条流(一条输入,一条输出)的读写与对方进行数据交换。
*/
private Socket socket;
/**
* 构造器,用于初始化客户端
*/
public Client(){
try {
System.out.println("正在链接服务端...");
/*
Socket实例化时就是与服务端建立链接的过程,此时需要传入两个
参数
参数1:服务端的IP地址,用于找到服务端的计算机
参数2:服务端口,用于找到服务端程序
如何查看IP地址:
windows:窗口键+R 打开控制台
输入ipconfig
查看以太网适配器-以太网,找到ipv4查看自己的IP地址
mac:打开[终端]程序
输入/sbin/ifconfig查看自己的IP地址
*/
// socket = new Socket("127.0.0.1",8088);//127.0.0.1和localhost都是表示本机
socket = new Socket("localhost",8088);
System.out.println("与服务端成功链接!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 用于客户端开始工作的方法
*/
public void start(){
/*
Socket提供的方法:
OutputStream getOutputStream()
通过Socket获取一个字节输出流,通过向该流写出字节,就可以发送给远端链接
的计算机的Socket了
*/
try {
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw, true);
pw.write(01)
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//socket的close方法会进行四次挥手
//并且也会关闭通过socket获取的输入流和输出流
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//实际开发中不会在main方法中写业务逻辑,main方法是静态方法会有很多不便
Client client = new Client();//调用构造器初始化客户端
client.start();//调用start方法使客户端开始工作
}
服务端
public class Server {
/*
java.net.ServerSocket
运行在服务端的ServerSocket相当于时客户中心的"总机",上面有若干的插座(Socket)
客户端的插座就是与总机建立链接,然后总机这边分配一个插座与之建立链接,来保持双方
通讯的。
ServerSocket有两个主要工作
1:创建时向系统申请服务端口,以便客户端可以通过端口找到
2:监听该端口,一旦一个客户端链接,便创建一个Socket,通过它与客户端通讯
*/
private ServerSocket serverSocket;
public Server(){
try {
System.out.println("正在启动服务端");
/*
实例化ServerSocket时需要指定向系统申请的服务端口,如果该端口
已经被系统的其他应用程序占据,则这里会抛出异常
java.net.BindException: Address already in use: bind()
*/
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void start(){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket的重要方法:
Socket accept()
该方法是一个阻塞方法,调用该方法后程序会"卡住",直到一个客户端使用
Socket与服务端建立链接为止,此时accept方法会立即返回一个Socket
通过返回的Socket就可以与链接的客户端双向通讯了。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String message;
while ((message = br.readLine()) != null) {
//张三[192.168.2.5]说:XXXX
System.out.println("客户端"+说:" + message);
} catch (IOException e) {
System.out.println(e);
}
}
问题 分析
问题 这个客户端只能固定写死输出的数据,不能根据用户输入的内容传递到服务端,使服务端读取数据
这个问题,需要解决的话客户端需要用的Scanner,替换之前单调输出 pw.write()
Scanner scanner = new Scanner(System.in);
while(true) {
String line = scanner.nextLine();
if("exit".equalsIgnoreCase(line)){
break;
}
pw.println(line);
}
需要提醒的是,Scanner中 nextline 是根据换行符鉴定用户是否输入一行内容,而PrintWriter是可以根据换行符刷新更新数据的,PrintWriter pw = new PrintWriter(bw, true);这边写了true是表示字符串输出流可以根据鉴定用户是否换行来刷新数据,记得 一定要使用printIn 或者 print(+"\n").
问题 假如多个客户端需要连接一个服务端怎么办
可以循环新建一个接受用户的插口
public void start(){
while(true){
try {
System.out.println("等待客户端链接...");
/*
ServerSocket的重要方法:
Socket accept()
该方法是一个阻塞方法,调用该方法后程序会"卡住",直到一个客户端使用
Socket与服务端建立链接为止,此时accept方法会立即返回一个Socket
通过返回的Socket就可以与链接的客户端双向通讯了。
*/
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in,StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String message;
while ((message = br.readLine()) != null) {
//张三[192.168.2.5]说:XXXX
System.out.println("客户端"+"说: " + message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
问题 服务端想要获取上线的用户名,同时获取客户端的ip地址
解决方式 客户端第一次传递给服务端的用户名为上线的用户名(这边使用昵称代替) ,这边客户端使用的是scanner获取用户的昵称
客户端代码
Scanner scanner = new Scanner(System.in);
//首先要求用户输入一个昵称
String nickname = "";
while(true) {
System.out.println("请输入昵称:");
nickname = scanner.nextLine();
if(nickname.trim().length() > 0){
pw.println(nickname);//将昵称发送给服务端
break;
}
System.out.println("昵称不能为空");
}
System.out.println("开始聊天吧");
服务端
服务端需要封装一个private 的昵称,并读取
public class Server {
private String nickname;
private ServerSocket serverSocket;
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
nickname = br.readLine();
}
同时 获取客户端的ip地址 是通过服务端接受的客户端插口的 inter地址和本机地址实现的,这个暂时先了解
ip = socket.getInetAddress().getHostAddress();//socket是服务端接受的客户端插口的对象
问题 多个客户端连接一个服务器,同时不同客户端给服务端发送消息,服务端只能接受第一个人发生来的消息
原因分析 第一 服务端start方法使用两个循环,内存循环除非执行完,才能执行外层循环,而内层循环的Scanner是获取用户的输入,而内层需要结束需要根据用户的输入和字符串比较才能得到的结果.
我们需要引入一个新的概念 线程 线程是将我们程序用一条线的方式按照顺序依次执行.
这个我打算整理完,再说,我也是一个刚学的新手😂😂