本篇文章分为两个部分,一个部分是总结使用Socket实现TCP的编程的知识,主要就是完成服务器端和客户端两个对象的代码编写;另一个部分是通过Java写一个聊天室来对我们的Socket编程进行巩固。
文章结构:首先是对TCP的简单介绍,然后在分析Socket通信的模型后进行Java Socket实现TCP编程的代码编写,最后是利用Socket的知识编写一个简单的聊天室。
1.TCP简介
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP,本篇文章不介绍UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。
首先了解一下网络模型中的数据传递:应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
2.Socket通信模型
所谓socket 通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过”套接字”向网络发出请求或者应答网络请求。首先我们看看Socket基于TCP的通信模型图:
图中通信模型的各个步骤如下:
- 在服务端建立一个ServerSocket,绑定相应的端口,并且在指定的端口进行侦听,等待客户端的连接。
- 当客户端创建连接Socket并且向服务端发送请求。
- 服务器收到请求,并且接受客户端的请求信息。一旦接收到客户端的连接请求后,会创建一个连接socket,用来与客户端的socket进行通信。通过相应的输入/输出流进行数据的交换,数据的发送接收以及数据的响应等等。
- 当客户端和服务端通信完毕后,需要分别关闭socket,结束通信。
也就是基于服务器和客户端的开发,所以在服务器端和客户端我们分别需要完成的代码就是:
对于服务器端需要完成的工作是:
- 创建ServerSocket对象,绑定监听器
- 通过accept()方法监听客户端请求
- 连接建立以后通过读取客户端发送请求消息
- 通过输出流向客户端发送响应信息
- 关闭资源
ServerSocket类中涉及到的常用方法:
- ServerSocket(int port)——创建并绑定到特定端口的服务器套接字
- accept()——侦听并接受到此套接字的连接
- close()——关闭此套接字
- getInetAddress()——得到ServerSocket对象绑定的IP地址。如果ServerSocket对象未绑定IP地址,返回0.0.0.0
- getLocalPort()——返回此套接字在其上侦听的端口
客户端需要完成的工作是:
- 创建Socket对象,指明需要连接的服务器地址和端口号(1023以后的端口,因为0~1023之间的端口号是我们系统需要使用的端口号)
- 连接建立后,通过输出流向服务器端请求
- 通过输入流获取服务器响应信息
- 关闭资源
Socket类中常用的方法:
- Socket(InetAddress address, int port)——创建一个套接字并将其连接到指定ip地址的指定端口号
- Socket(String host, int port)——创建一个套接字并将其连接到指定主机上的指定端口号
- close()——关闭此套接字
- getInetAddress()——返回套接字连接的地址
- getInputStream()——返回此套接字的输入流
- getOutputStream——返回此套接字的输出流
好了,通过上述的描述,对Socket的编程就讲述的很清楚了,接下来针对上述描述进行我们服务器和客户端代码的编码工作。
3.基于Tcp的Socket开发代码编写
首先是服务器端Server.java的代码编写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56/**
* Created by codingBoy on 16/11/29.
* 基于TCP协议的Socket通信,实现客户登录
*/
public class Server
{
public static void main(String[] args)
{
//1.创建一个服务器Socket,即ServerSocket,指定绑定的端口,并坚挺
try {
ServerSocket serverSocket=new ServerSocket(8888);
//2,调用accept()开始监听,等待客户端的链接
System.out.println("****服务器即将启动,等待客户端的连接****");
Socket socket=serverSocket.accept();
//3.获取输入流并获取客户信息
InputStream in=socket.getInputStream();
InputStreamReader isr=new InputStreamReader(in,"utf-8");
BufferedReader br=new BufferedReader(isr);
String info;
StringBuilder sb=new StringBuilder();
while ((info=br.readLine())!=null)
{
sb.append(info);
}
System.out.println("我是服务器,客户端发来的消息为:"+sb);
socket.shutdownInput();//关闭输入流
//4.获取输出流,用于响应客户端的请求
OutputStream os=socket.getOutputStream();
PrintWriter pw=new PrintWriter(os);
pw.write("欢迎您");
pw.flush();//将缓冲输出
//4.关闭相关资源
pw.close();
os.close();
br.close();
isr.close();
in.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后是客户端Client.java的代码编写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42public class Client {
public static void main(String[] args)
{
//1.创建客户端Socket,指定服务器端地址和端口号
try {
Socket socket=new Socket("localhost",8888);
//2.获取输出流,用来向服务器端发送登录信息
OutputStream os=socket.getOutputStream();
PrintWriter pw=new PrintWriter(os);//将输出流打包成打印流
pw.write("用户名:codingxiaxw;密码:123");
pw.flush();//刷新缓存
socket.shutdownOutput();//关闭输出流
//3.获取服务器传过来的输入流,读取服务器的响应信息
InputStream in=socket.getInputStream();
BufferedReader br=new BufferedReader(new InputStreamReader(in,"utf-8"));
String info;
StringBuilder sb=new StringBuilder();
while ((info=br.readLine())!=null)
{
sb.append(info);
}
System.out.println("我是客户端,服务器给我的信息为:"+sb);
//3.关闭资源
br.close();
in.close();
pw.close();
os.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此时我们的客户端和服务器端的代码便完成了,首先运行服务器Server.java,代码在执行到Socket socket=serverSocket.accept();
时会在此阻塞,控制台输出:1
****服务器即将启动,等待客户端的连接****
直到等到客户端连接到该端口号的服务器后服务器的代码才会向下执行,此时运行客户端Client.java,客户端的控制台输出:1
2我是客户端,服务器给我的信息为:欢迎您
Process finished with exit code 0
然后此时切换到服务器的控制台,发现输出信息:1
2
3
4****服务器即将启动,等待客户端的连接****
我是服务器,客户端发来的消息为:用户名:codingxiaxw;密码:123
Process finished with exit code 0
此时我们便使用Socket完成了一个服务器和一个客户端之间的通信,那么问题来了,如何实现一个服务器与多个客户端之间的通信呢?我们使用多线程服务器的方式。创建一个ServerThread.java用于编写服务器端多线程接收客户端传递过来的信息的代码编写,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58public class ServerThread extends Thread
{
private Socket socket;
public ServerThread(Socket socket)
{
this.socket=socket;
}
public void run()
{
//3.获取输入流并获取客户信息
InputStream in= null;
InputStreamReader isr=null;
BufferedReader br=null;
OutputStream os=null;
PrintWriter pw=null;
try {
in = socket.getInputStream();
isr=new InputStreamReader(in,"utf-8");
br=new BufferedReader(isr);
String info;
StringBuilder sb=new StringBuilder();
while ((info=br.readLine())!=null)
{
sb.append(info);
}
System.out.println("我是服务器,客户端发来的消息为:"+sb);
socket.shutdownInput();//关闭输入流
//4.获取输出流,用于响应客户端的请求
os=socket.getOutputStream();
pw=new PrintWriter(os);
pw.write("欢迎您");
pw.flush();//将缓冲输出
} catch (IOException e) {
e.printStackTrace();
}finally {
//4.关闭相关资源
try {
if (pw!=null) pw.close();
if (os!=null) os.close();
if (br!=null) br.close();
if (isr!=null) isr.close();
if (in!=null) in.close();
if (socket!=null) socket.close();
}catch (Exception e)
{
e.printStackTrace();
}
}
}
}
实际上我们就是将之前写在Server.java中获取客户端传递过来的信息的那部分代码拿出来写在了该线程中,然后修改Server.java中的代码为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class Server
{
public static void main(String[] args)
{
//1.创建一个服务器Socket,即ServerSocket,指定绑定的端口,并坚挺
try {
ServerSocket serverSocket=new ServerSocket(8888);
//2,调用accept()开始监听,等待客户端的链接
System.out.println("****服务器即将启动,等待客户端的连接****");
//记录客户端的数量
int count=0;
while (true) {
//调用accept()方法开始监听,等待客户端的连接
Socket socket = serverSocket.accept();
//创建一个新的线程
ServerThread serverThread=new ServerThread(socket);
//启动线程
serverThread.start(); //如果不要启动线程的话这里直接调用run()也行.
count++;//统计客户端的数量
System.out.println("客户端的数量:"+count);
InetAddress address=socket.getInetAddress();
System.out.println("当前客户端的IP:"+address.getHostAddress());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行Server.java,服务器控制台输出信息:1
****服务器即将启动,等待客户端的连接****
然后运行Client.java,客户端控制台输出信息:1
我是客户端,服务器给我的信息为:欢迎您
此时跳转到服务器控制台,信息变为:1
2
3
4****服务器即将启动,等待客户端的连接****
我是服务器,客户端发来的消息为:用户名:codingxiaxw;密码:123
客户端的数量:1
当前客户端的IP:127.0.0.1
然后更改Client.java中传递给服务器的数据代码为:pw.write("用户名:codingxiaxw;密码:456");
,再运行Client.java,发现服务器的控制台输出信息变为:1
2
3
4
5
6
7****服务器即将启动,等待客户端的连接****
我是服务器,客户端发来的消息为:用户名:codingxiaxw;密码:13
客户端的数量:1
当前客户端的IP:127.0.0.1
我是服务器,客户端发来的消息为:用户名:codingxiaxw;密码:456
客户端的数量:2
当前客户端的IP:127.0.0.1
这样我们便完成了多线程服务器的编码工作,到此我们便成功使用Socket完成了基于TCP协议的编程。接下来趁热打铁,用Socket实现一个简单的聊天室功能。
4.使用Socket实现一个简单的聊天室
功能概述:客户端用于发送信息(在控制台中发送信息),将在控制台输出的信息转换为输出流输出到服务器端,服务器通过socket.getOutputStream()
方法接收客户端传来的信息,并将发送该信息的客户端地址及信息发送在控制台中,这样我们便简单的实现了我们的聊天室。
这里给大家讲讲qq通信的原理:qq使用c/s模式进行通信,qq中的用户A发送信息给用户B,过程是这样的:当A打开和B的聊天窗口时即和B还有服务器建立了一个聊天室(同时服务器和客户端开启连接),A发送信息,其实是发送到了qq聊天室服务器的接收容器中,然后qq服务器将该客户端地址(即qq头像)和信息内容显示在聊天室中(即聊天窗口),你每和一个好友进行聊天打开一个窗口就等于和她(另一个客户端)还有我们的qq服务器组成了一个聊天室(当然聊天室的服务器肯定是多线程的)。qq上还有多人聊天的功能,实现道理也是这样,只是该聊天室中有多个客户端给服务器发送消息罢了。(这是qq刚兴起时的聊天功能设计,也就是我们本篇文章需要实现的简单的聊天室功能,下面的内容与设计聊天室无关,但是我觉得还是有必要跟大家介绍清楚如今的qq时怎样工作的,了解便可)
qq服务器挂在腾讯的某台主机上,相当于起了一个中转站的成分,这种聊天功能的实现对于客户端数量比较少时服务器端还能接受,但是在客户端数量很多时服务器肯定要瘫痪。
所以为了减少服务器端的压力,需要实现客户端和客户端之间的直接通信,这样客户端上的qq既要实现服务器端的功能(用于接收信息)又要实现客户端的功能(用于发送信息)。此时qq服务器就不再作为一个中转站的功能了,它主要用于:用于客户端程序登陆,验证用户名密码,获取其他在线好友信息等等。
分析了功能后接下来进行我们服务器端和客户端代码的编写,首先创建一个ChatRoom.java,运行后用于开启服务器和客户端的连接,代码如下:1
待更新。
待更新。
2018.3.19更
欢迎加入我的Java交流1群:659957958。
2018.4.21更:如果群1已满或者无法加入,请加Java学习交流2群:305335626 。
5.联系
If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.