- 在现实网络传输应用中,
通常使用TCP
、IP
或UDP
这3种协议实现数据传输
。
在传输数据
的过程中,
需要通过一个双向的通信连接
实现数据的交互
。
在这个传输过程
中,
通常将这个双向链路的一端
称为Socket
,
一个Socket
通常由一个IP地址
和一个端口号
来确定。
在整个数据传输过程中
,Socket
的作用是巨大的。
在Java编程应用中,Socket
是Java网络编程的核心。
Socket基础
- 在
网络编程
中有两个主要的问题
,
一个是如何准确地定位
网络上一台或多台主机,
另一个就是找到主机后
如何可靠高效地
进行数据传输
。
在TCP/IP协议
中
IP层
主要负责网络主机的定位
,数据传输
的路由
,
由IP地址
可以唯一地确定
Internet上的一台主机
。
TCP层
则
提供面向应用的可靠(TCP)
的
或非可靠(UDP)
的数据传输机制
,
这是网络编程
的主要对象
,
一般不需要关心IP 层
是如何处理数据
的。
目前较为流行的网络编程模型
是客户机/服务器(C/S)结构
。
即通信双方
,一方作为服务器
等待(另一方作为的)客户
提出请求
并予以响应
。
客户则在需要服务时
向服务器提出申请
。
服务器一般作为守护进程
始终
运行,
监听网络端口,
一旦有客户请求,就会启动一个服务进程
来响应该客户,
同时自己继续监听服务端口
,
使后来的客户
也能及时得到服务
。
TCP/IP协议基础
TCP/IP
是Transmission Control Protocol/Internet Protocol
的简写,
中译名为传输控制协议
/因特网协议
,
又名网络通信协议
,
是Internet最基本的协议
、Internet国际互联网络
的基础
,
由网络层
的IP协议
和
传输层
的TCP协议
组成。
TCP/IP
定义了电子设备
如何连入因特网
,
以及数据
如何在它们之间传输的标准
。
TCP/IP协议
采用了4层
的层级结构
,
每一层
都呼叫它的下一层
所提供的协议
来完成自己的需求
。
也就是说,
TCP
负责发现传输
的问题,
一旦发现问题
便发出信号
要求重新传输
,
直到所有数据
安全正确地传输到目的地
。
而IP
的功能是给因特网的每一台电脑规定
一个地址
。
TCP/IP协议
不是TCP
和IP
这两个协议的合称
,
而是指因特网整个TCP/IP协议簇
。
从协议分层模型
方面来讲,TCP/IP
由4个层次
组成,
分别是网络接口层
、网络层
、传输层
、应用层
。其实
TCP/IP
协议并不完全符合OSI(Open System Interconnect)
的7层参考模型
,
OSI
是传统的开放式系统互连参考模型
,
是一种通信协议
的7层抽象
的参考模型
,
其中每一层
执行某一特定任务
。
该模型的目的
是
使各种硬件
在相同的层次
上相互通信
。
这7层
是
物理层、数据链路层(网络接口层)
、
网络层(网络层)
、
传送层(传输层)
、
会话层、表示层和应用层(应用层)
。
而TCP/IP协议
采用了4层
的层级结构,
每一层
都呼叫它的下一层
所提供的网络
来完成自己的需求
。
由于ARPANET
的设计者注重的是网络互联
,
允许通信子网(网络接口层)
采用已有的
或是将来有的各种协议
,
所以这个层次中没有提供专门的协议
。
实际上,
TCP/IP协议
可以通过网络接口层
连接到任何网络
上,
例如X.25交换网
或IEEE802局域网
。
UDP协议
UDP
是User Datagram Protocol
的简称,
是一种无连接
的协议,
每个数据报
都是一个独立的信息
,
包括完整
的源地址
或目的地址
,
它在网络上以任何可能的路径
传往目的地
,
因此能否到达目的地
,
到达
目的地的时间
以及内容的正确性
都是不能被保证
的。
在现实网络数据传输过程中
,
大多数
功能是由TCP协议
和UDP协议
实现。(1)TCP协议
面向连接
的协议,
在Socket
之间进行数据传输
之前必然要建立连接
,
所以在TCP
中需要连接时间
。
TCP传输数据大小限制
,
一旦连接建立
起来,
双方的Socket
就可以按统一的格式
传输大的数据
。
TCP是一个可靠的协议
,
它确保接收方完全正确地
获取发送方
所发送的全部数据
。(2)UDP协议
每个数据报
中都给出了完整
的地址信息
,
因此无需要建立发送方
和接收方
的连接
。
UDP传输数据时是有大小限制
的,
每个被传输的数据报
必须限定在64KB
之内。
UDP是一个不可靠
的协议,
发送方
所发送的数据报
并不一定以相同的次序
到达接收方
。
TCP、UDP选择的决定因素
(1)TCP在
网络通信
上有极强
的生命力
,
例如远程连接(Telnet)
和文件传输(FTP)
都需要不定长度
的数据
被可靠地传输
。
但是可靠的传输
是要付出代价的,
对数据内容正确性的检验
必然占用计算机的处理时间
和网络的带宽
,
因此TCP传输
的效率不如UDP高
。(2)UDP
操作简单
,而且仅需要较少的监护
,
因此通常用于局域网高可靠性
的分散系统
中Client/Server 应用程序
。
例如视频会议系统
,
并不要求音频视频数据
绝对的正确
,
只要保证连贯性
就可以了,
这种情况下显然使用UDP
会更合理
一些,
因为TCP
和UDP
都能达到这个保证连贯性
的门槛,
但是TCP
却要多占用更多的计算机资源
,
杀鸡焉用牛刀
呢,
所有这种情况不用TCP
,用UDP
。
基于Socket的Java网络编程
网络上的两个程序通过一个
双向
的通信连接
实现数据的交换
,
这个双向链路
的一端
称为一个Socket
。Socket
通常用来实现客户方
和服务方
的连接。Socket
是TCP/IP协议
的一个十分流行
的编程方式,
一个Socket
由一个IP地址
和一个端口号
唯一确定
。
但是,
Socket
所支持的协议种类
也不光TCP/IP
一种,
因此两者之间是没有必然联系
的。
在Java环境
下,
Socket编程
主要是指基于TCP/IP协议
的网络编程
。
1.Socket通信的过程
Server
端Listen(监听)
某个端口
是否有连接请求
,
Client端
向Server 端
发出Connect(连接)请求
,
Server端
向Client端
发回Accept(接收)消息
,
一个连接就建立起来了。Server端
和Client端
都可以通过Send
、Write
等方法与对方通信
。在
Java网络编程应用
中,
对于一个功能齐全的Socket
来说,
其工作过程包含如下所示的基本步骤。
(1)创建ServerSocket
和Socket
;
(2)打开连接到Socket
的输入/输出流
;
(3)按照一定的协议对Socket
进行读/写操作
;
(4)关闭IO流
和Socket
。
2.创建Socket
在
Java网络编程应用
中,
包java.net
中提供了两个类Socket
和ServerSocket
,
分别用来表示双向连接
的客户端
和服务端
。
这是两个封装得非常好的类,
其中包含了如下所示的构造方法
:Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int prot);
Socket(String host, int prot, boolean stream);
Socket(SocketImpl impl);
Socket(String host, int port, InetAddress localAddr, int localPort);
Socket(InetAddress address, int port, InetAddress localAddr, int localPort);
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr)
在上述
构造方法
中,
参数address
、host
和port
分别是
双向连接
中另一方
的IP地址
、主机名
和端口号
,
stream
指明Socket
是流Socket
还是数据报Socket
,
localPort
表示本地主机
的端口号
,
localAddr
和bindAddr
是本地机器的地址
(ServerSocket
的主机地址
),
impl
是Socket
的父类
,
既可以用来创建ServerSocket
又可以用来创建Socket
。
例如:
Socket client = new Socket("127.0.0.1", 80);
ServerSocket server = new ServerSocket(80);
- 注意:
必须小心地选择端口
,
每一个端口
提供一种特定的服务
,
只有给出正确的端口
,才能获得相应的服务
。
0~1023
的端口号
为系统所保留
,
例如
HTTP
服务的端口号为80
,
Telnet
服务的端口号为21
,
FTP
服务的端口号为23
,
所以我们在选择端口号时
,最好选择一个大于1023
的数
以防止发生冲突
。
另外,
在创建Socket
时如果发生错误
,将产生IOException
,
在程序中
必须对之做出处理
。
所以在创建Socket
或ServerSocket
时必须捕获
或抛出异常
。
TCP编程详解
TCP/IP通信协议
是一种可靠
的网络协议
,
能够在通信的两端
各建立一个Socket
,
从而在通信的两端
之间形成网络虚拟链路
。
一旦建立了虚拟的网络链路
,
两端的程序
就可以通过虚拟链路
进行通信
。
Java语言对TCP网络通信
提供了良好的封装
,
通过Socket对象
代表两端
的通信端口
,
并通过Socket
产生的IO流
进行网络通信
。
这里先笔记Java应用
中TCP编程的基本知识,
为后面的Android编程
打下基础。
使用ServerSocket
在Java程序中,
使用
类ServerSocket
接受其他通信实体
的连接请求
。
对象ServerSocket
的功能是监听
来自客户端的Socket连接
,
如果没有连接
则会一直处于等待状态
。在类
ServerSocket
中包含了如下监听客户端连接请求的方法:
Socket accept()
:如果接收到一个客户端Socket
的连接请求
,
该方法将返回
一个与客户端Socket
对应的Socket
,
否则该方法
将一直处于等待状态
,线程也被阻塞
。-
为了创建
ServerSocket对象
,
ServerSocket类
为我们提供了如下构造器
:ServerSocket(int port)
:
用指定的端口port
创建一个ServerSocket
,
该端口
应该是有一个有效
的端口整数值0~65535
。ServerSocket(int port,int backlog)
:
增加一个用来改变连接队列长度
的参数backlog
。ServerSocket(int port,int backlog,InetAddress localAddr)
:
在机器(服务器、本机等)
存在多个IP地址
的情况下,
允许通过localAddr
这个参数
来指定将ServerSocket
绑定到指定的IP地址
。
当使用
ServerSocket
后,
需要使用ServerSocket
中的方法close()
关闭该ServerSocket
。在通常情况下,
因为服务器不会只接受
一个客户端请求
,
而是会不断地接受
来自客户端
的所有请求
,
所以可以通过循环
来不断
地调用ServerSocket
中的方法accept()
。例如下面的代码。
//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket ss = new ServerSocket(30000);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = ss.accept();
//下面就可以使用Socket进行通信了
...
}
- 在上述代码中,
创建的ServerSocket
没有指定IP地址
,
该ServerSocket
会绑定
到本机默认
的IP地址
。
在代码中使用30000
作为该ServerSocket
的端口号
,
通常推荐使用10000
以上的端口
,
主要是为了避免与其他应用程序
的通用端口
冲突
。
使用Socket
-
在客户端可以使用
Socket
的构造器
实现``和指定服务器
的连接
,
在Socket
中可以使用如下两个构造器:Socket(InetAddress/String remoteAddress, int port)
:
创建连接到指定远程主机
、远程端口的Socket
,
该构造器没有指定本地地址
、本地端口
,
本地IP地址
和端口
使用默认值
。Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort)
:
创建连接到指定远程主机
、远程端口
的Socket
,
并指定本地IP地址
和本地端口号
,
适用于本地主机
有多个IP地址
的情形。
在使用上述
构造器
指定远程主机
时,
既可使用InetAddress
来指定,也可以使用String对象
指定,
在Java
中通常使用String对象
指定远程IP
,例如192.168.2.23
。
当本地主机只有一个IP地址时,建议使用第一个方法,简单方便。
例如下面的代码:
//创建连接到本机、30000端口的Socket
Socket s = new Socket("127.0.0.1" , 30000);
当程序执行
上述代码
后会连接到指定服务器
,
让服务器端
的ServerSocket
的方法accept()
向下执行,
于是服务器端
和客户端
就产生一对互相连接的Socket
。
上述代码连接到“远程主机”的IP地址是127.0.0.1
,
此IP地址
总是代表本机的IP地址
。
这里例程的服务器端
、客户端
都是在本机
运行,
所以Socket
连接到远程主机
的IP地址
使用127.0.0.1。当
客户端
、服务器端
产生对应的Socket
之后,
程序无须再区分服务器端和客户端,
而是通过各自的Socket进行通信。-
在
Socket
中提供如下两个方法获取输入流
和输出流
:InputStream getInputStream()
:
返回该Socket对象
对应的输入流
,
让程序通过该输入流
从Socket中取出数据。OutputStream getOutputStream()
:
返回该Socket对象
对应的输出流
,
让程序通过该输出流
向Socket
中输出数据
。
TCP协议的服务器端例程:
public class Server
{
public static void main(String[] args)
throws IOException
{
//创建一个ServerSocket,用于监听客户端Socket的连接请求
ServerSocket myss = new ServerSocket(30001);
//采用循环不断接受来自客户端的请求
while (true)
{
//每当接受到客户端Socket的请求,服务器端也对应产生一个Socket
Socket s = myss.accept();
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
//进行普通IO操作
ps.println("凌川江雪!");
ps.println("望川霄云!");
ps.println("万年太久,只争朝夕!");
ps.println("人间正道是沧桑!");
ps.println("穷善其身,达济天下!");
//关闭输出流,关闭Socket
ps.close();
s.close();
}
}
}
- 上述代码建立了
ServerSocket监听
,
并且使用Socke
t获取了输出流
,
执行后不会显示任何信息。 - 对应的TCP协议的客户端例程:
public class Client {
public static void main(String[] args) throws IOException
{
Socket socket = new Socket("127.0.0.1" , 30001);
//将Socket对应的输入流包装成BufferedReader
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
//进行普通IO操作
StringBuilder response = new StringBuilder();
String line;
//一行一行地读取并加进stringbuilder
while((line = br.readLine()) != null){
response.append(line + "\n");
}
System.out.println("来自服务器的数据:" + "\n" + response.toString());
//关闭输入流、Socket
br.close();
socket.close();
}
}
-
上述代码使用Socket建立了与指定IP、指定端口的连接,
并使用Socket获取输入流读取数据,
之后处理一下数据然后打印在工作台。
先
运行服务端Class
,再
运行客户端Class
,运行结果: 由此可见,
一旦使用ServerSocket
和Socket
建立网络连接
之后,
程序通过网络通信
与普通IO
并没有太大的区别。
如果先运行上面程序中的Server
类,
将看到服务器一直处于等待状态
,
因为服务器使用了死循环
来接受来自客户端
的请求;
再运行Client
类,
将可看到程序输出“来自服务器的数据:...!”,
这表明客户端和服务器端通信成功。
TCP中的多线程
刚刚实操的例程中,
Server
和Client
只是进行了简单的通信操作,
当服务器接收到客户端连接之后,服务器向客户端输出一个字符串,
而客户端
也只是读取
服务器的字符串后
就退出
了。在实际应用中,
客户端
可能需要和服务器端
保持长时间通信
,
即服务器
需要不断
地读取客户端数据
,
并向客户端写入
数据,
客户端
也需要不断
地读取
服务器数据,
并向服务器写入
数据。当使用
readLine()
方法读取数据
时,
如果在该方法成功返回之前线程
被阻塞
,则程序无法继续执行
。
所以服务器
很有必要为每个Socket
单独启动一条线程
,
每条线程
负责与一个客户端
进行通信
。另外,
因为客户端
读取服务器数据
的线程
同样会被阻塞
,
所以系统
应该单独
启动一条线程
,
该组线程
专门负责读取服务器数据
。假设要开发一个
聊天室程序
,
在服务器端
应该包含多条线程
,
其中每个Socket对应一条线程
,
该线程
负责
读取 Socket 对应输入流
的数据
(从客户端
发送过来的数据
),
并将读到的数据
向每个Socket输出流
发送一遍
(将一个客户端
发送的数据
“广播”
给其他客户端
);因此需要在
服务器端
使用List
来保存所有的Socket
。
在具体实现
时,
为服务器
提供了如下两个类
:
创建ServerSocket监听
的主类
。
处理每个Socket通信
的线程类
。
1/4 接下来介绍具体实现流程,首先看下面的IServer
Class:
public class IServer
{
//定义保存所有Socket的ArrayList
public static ArrayList socketList = new ArrayList();
public static void main(String[] args)
throws IOException
{
ServerSocket ss = new ServerSocket(30000);
while(true)
{
//此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
//每当客户端连接后启动一条ServerThread线程为该客户端服务
new Thread(new Serverxian(s)).start();
}
}
}
IServer
类中,
服务器端(ServerSocket )
只负责接受客户端Socket
的连接请求
,
每当客户端Socket
连接到该ServerSocket
之后,
程序将客户端对应的Socket(客户Socket的对面一端)
加入socketList集合
中保存
,
并为该Socket
启动一条线程
(Serverxian
),
该线程
负责处理 该Socket所有 的 通信任务
。
小结:
IServer
类完成的业务是:
1.接收客户端Socket
,
2.保存对应返回的Socket
,
3.启动处理线程
。
2/4 接着看服务器端线程类文件:
package liao.server;
import java.io.*;
import java.net.*;
import java.util.*;
//负责处理每个线程通信的线程类
public class Serverxian implements Runnable
{
//定义当前线程所处理的Socket
Socket s = null;
//该线程所处理的Socket所对应的输入流读取器
BufferedReader br = null;
public Serverxian(Socket s)
throws IOException
{
this.s = s;
//初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
public void run()
{
try
{
String content = null;
//采用循环不断从Socket中读取客户端发送过来的数据
while ((content = readFromClient()) != null)
{
//遍历socketList中的每个Socket,
//将读到的内容向每个Socket发送一次
for (Socket s : IServer.socketList)
{
//将Socket对应的输出流包装成PrintStream
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}
catch (IOException e)
{
//e.printStackTrace();
}
}
//定义读取客户端数据的方法
private String readFromClient()
{
try
{
return br.readLine();
}
//如果捕捉到异常,表明该Socket对应的客户端已经关闭
catch (IOException e)
{
//删除该Socket。
IServer.socketList.remove(s);
}
return null;
}
}
Serverxian类(服务器端线程类)中,
注意是线程类,继承Runnable,重写run方法
会不断读取客户端数据,
在获取时使用方法readFromClient()来读取客户端数据。
如果读取数据过程中捕获到 IOException异常,
则说明此Socket对应的客户端Socket出现了问题,
程序就会将此Socket从socketList中删除。
当服务器线程读到客户端数据之后会遍历整个socketList集合,
并将该数据向socketList集合中的每个Socket发送一次,
该服务器线程将把从Socket中读到的数据
向socketList中的每个Socket转发一次。
上述代码能够不断获取
Socket
输入流中的内容,
当获取Socket输入流
中的内容
后,
直接将这些内容
打印在控制台
。
先运行上面程序中的类IServer
,
该类运行后作为本应用的服务器,不会看到任何输出。接着可以运行多个 IClient——相当于启动多个聊天室客户端登录该服务器,此时在任何一个客户端通过键盘输入一些内容后单击“回车”键,将可看到所有客户端(包括自己)都会在控制台收到刚刚输入的内容,这就简单实现了一个聊天室的功能。-
运行结果如下动图所示:
(这个链接是
在Eclipse上,同时运行多个java程序,
用不同的console显示运行信息的方法)
同时启动两个客户端,
来回切换客户端进行“聊天”,
客户端由于服务端的socket传输,
可以相互收到彼此的信息;