本文学习并总结java Socket网络程序。目前学习的是网络模型中的网络层和传输层在java中的体现和使用,后续学习java Web开发时就是在应用层编程了。本文先描述了网络通讯3要素及其在java中的封装,后面重点讲述java UDP DatagramSocket编程和TCP Socket/ServerSocket编程,关于UDP编程,本文给出了一个自己编写的1对1聊天程序(带图形化界面)。
网络通讯要素
1. IP地址
用于标识一个主机,IPv4地址长度为32位,IPv6版本地址长度为128位(地址包含字母)。
IP地址一般都有一个主机名与之对应,windows下IP地址与主机名映射关系文件在C:\WINDOWS\System32\drivers\etc\hosts中,linux下映射文件上/etc/hosts中。
局域网中IP地址可以比较随意,只要在同一个网段即可。192.168.0.0~192.168.255.255,10.0.0.0~10.255.255.255,172.16.0.0~172.31.255.255属于私有地址,专门为组织机构内部使用,互联网上是不使用,而被用在局域网中。
127.0.0.1,本机回环地址,可用于测试本机网卡及TCP/IP协议簇是否已正确安装。
2. 逻辑端口
用于标识一个主机上的不同进程,是软件上定义的端口,而非实质的物理端口。一个网络应用程序对应一个或数个端口。
端口号取值范围是0~65535,但0~1024被保留给系统程序使用,几个常用默认端口:
http协议服务器默认端口是80;
tomcat服务器默认端口是8080;
ftp默认端口是21,sftp默认端口是22;
telnet默认端口是23;
my sql数据库默认端口是3306;
oralce数据库默认端口是1521。
3. 通信协议
通信双方必须安装相同的通信协议,不同协议的一方无法介入;一个组织或团体为安全起见会使用特殊的协议,其它第三方都无法介入。
国际组织定义了通用的TCP/IP和UDP协议,通用协议可用于局域网通信,还可用于广域网通信;IPX/NetBIOS是常用的局域的通信,只能用于局域网。
网络模型
ISO组织定义了OSI七层参考模型,从上到下依次是:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。OSI模型只是理论上的模型,实际的国际标准是TCP/IP四层模型,从上到下依次是:应用层、传输层、网络层、物理层,这个模型是美国国防部高级项目研究局在20世纪70年代的研究成果。
每一层都有自己使用的协议,应用层常用协议有http、ftp等, 传输层常用协议是TCP和UDP, 网络层常用协议是IP协议。网络数据从一台主机发送到另一台主机时是先从上到下经过各层进行数据封包,由物理层传输到目的地时再逐层解包。
用java语言来操作网络通信3个要素,封装成对象
InetAddress封装了IP地址,是Object的子类,它有2个直接子类:Inet4Address和Inet6Address。
InetAddress类没有构造函数,而类中有非静态方法,因此类中必须静态方法可以获取本类对象:
static InetAddress getByAddress(byte[] addr),给定原始IP,返回InetAddress对象。
static InetAddress getByAddress(String host, byte[] addr),根据主机名和IP地址,创建InetAddress对象。
static InetAddress getByName(String host), 根据主机名确定IP地址。
static InetAddress getLocalHost(), 返回本地主机。
该类中常用的非静态方法有:
String getHostAddress(),返回IP地址字符串。
String getHostName(), 返回IP地址主机名,如果IP地址与主机名的映射关系没有在网络上,则返回的还是IP地址。
网络上一个主机名,如百度可能对应多个IP地址,因为百度服务器有很多个,InetAddress类中有一个静态方法就是可以根据主机名获取所有IP地址对象:
static InetAddress[] getAllByName(String host), 给定主机名,返回所有IP地址的数组。
TCP和UDP
TCP(传输控制协议)和UDP(用户数据包协议),都是传输层的常用协议,2者的区别是:
UDP:
1. 无连接,数据封装成数据包,源数据包中携带地址,找不到目的地时包丢掉。
2. 每个数据包<=64K, 大量数据时要分包发送。
3. 不可靠
4. 不需要建立连接,速度快。
QQ聊天就是UDP传输,网络视频会议、桌面共享等追求速度快,会数据丢包要求不严格的应用都使用UDP。
TCP:
1. 面向连接,通过三次握手先确认对方是否可通信。
2. 可直接进行大数据量传输。
3. 可靠
4. 需要建立连接,效率低。
Socket
Socket是为网络服务提供的一种机制,通信2端都有Socket,网络通信其实就是Socket间的通信, 数据在2个Sokcet间通过IO流传输。
UDP网络编程
需要分别定义UDP发送端和接收端。
这里先记录一下UDP网络编程所要用的几个类及常用方法,再给出代码示例和练习。
DatagramSocket类
表示用来接收和发送数据报包的套接字,是Object类的子类,常用构造方法有:
DatagramSocket(),构造数据报套接字并将其绑定到本地主机上任何可用的端口。
DatagramSocket(int port), 创建数据报套接字并将其绑定到本地主机上的指定端口。
DatagramSocket(int port, InetAdress ipaddr), 创建数据报套接字,将其绑定到指定地址。
这3个构造方法都会抛SocketException。
常用方法:
void send(DatagramPacket dp), 从此套接字发送数据报包。
void receive(DatagramPacket dp), 从此套接字接收数据报包。
这2个方法都会抛IOException异常。
DatagramSocket类中还有getInetAddress()、getPort()等获取IP地址、主机名和端口号的多个方法,具体可查阅API手册。
DatagramPacket类
表示数据报包,继承自Object类,常用构造方法:
DatagramPacket(byte[] buf , int length),字节数组中存放要发送或接收的数据。
DatagramPacket(byte[] buf , int length, InetAddress address, int port), 数据报包中携带要发送的目的地址和端口。
DatagramPacket(byte[] buf , int offset, int length),发送字节数组指定位置上的数据或接收数据到字节数组指定位置上。
DatagramPacket(byte[] buf , int offset, int length, InetAddress address, int port),指定字节数组位置,并指定发送目的地址和IP。
常用方法:
InetAddress getAddress()、int getPort, 返回数据报将要发往该机器或从该机器接收数据的IP地址、端口。
void setAddress(InetAddress address),void setPort(int port), 设置数据报将要发往的那台机器的IP地址、端口。
String getData(), 返回数据缓冲区。
void setData(byte[] buf), 设置数据缓冲区。
int getLength()、void setLength(int len), 获取、设置数据报包的长度。
Udp通信的一个简单标准流程如下:
/*
需求:定义一个UDP发送端
步骤:
1. 建立udpsocket服务。
2. 提供数据,并将数据封装到数据包中,并指定数据要发往的机器原IP地址和端口。
3. 通过socket服务的发送功能,将数据包发送出去。
4. 关闭资源
*/
import java.net.*;
public class UdpSend {
public static void main(String[] args) throws Exception
{
//建立udpsocket服务
DatagramSocket ds=new DatagramSocket();
//提供数据,并将数据封装到数据包中,并指定数据要发往的机器原IP地址和端口
byte[] buf="hello udp!".getBytes();
DatagramPacket dp=new DatagramPacket(buf,buf.length,InetAddress.getByName("localhost"),1000);
//通过socket服务的发送功能,将数据包发送出去
ds.send(dp);
//关闭资源
ds.close();
}
}
/*
需求:定义一个UDP接收端
步骤:
1. 定义udpsocket服务,监听一个指定端口。
2. 定义一个字节数组,存储接收到的字节数据。
3. 通过socket服务的receive方法,将收到的数据存入已定义好的数据包中。
4. 通过数据包对象的特有功能,将这些不同的数据取出,进行处理。
5. 关闭资源。
*/
import java.net.*;
public class UdpRece
{
public static void main(String[] args) throws Exception
{
//定义udpsocket服务,监听一个指定端口
DatagramSocket ds=new DatagramSocket(10000);
//定义一个字节数组,存储接收到的字节数据,将该数组封装成数据报包对象
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
//通过socket服务的receive方法,将收到的数据存入已定义好的数据包中
ds.receive(dp);
//通过数据包对象的特有功能,将这些不同的数据取出,进行处理
String ip=dp.getAddress().getHostAddress();
String data=new String(dp.getData(),0,dp.getLength());
int port=dp.getPort();
System.out.println(ip+"..."+data+"...");
//关闭资源
ds.close();
}
}
前面说过QQ聊天就是用的udp协议进行通信的,下面是自己写的一个udp一对一聊天软件,界面较简单,后续会继续完善和美化。
/*
用UDP实现一对一聊天,采用图形化界面及多线程,可同时收发消息。
*/
import java.io.*;
import java.net.*;
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class UdpChat implements Runnable, ActionListener{
//定义图形化界面所需要的对象和变量
//主窗口
JFrame mainJframe;
//showArea用于显示聊天记录,msgArea存储待发送的文本消息
JTextArea showArea,msgArea;
//定义3个标签,分别标识UDP发送端端口、接收端端口、对方IP地址
JLabel lab1,lab2,lab3;
//定义3个文本框,供用户输入UDP发送端端口、接收端端口、对方IP地址
JTextField sendPortText,recePortText,IpAddressText;
//startBtn用于启动UDP服务,sendBtn用于发送消息
JButton startBtn, sendBtn;
//窗体整体采用边界布局BorderLayout, 由3个Jpanel容器构成,分别位于NORTH、DENTER、SOUTH
JPanel pane1,pane2,pane3;
//con用于表示窗体容器
Container con;
//定义3个JPanel容器的背景颜色,方便后面使用
Color blue = new Color(157, 242, 173);
//定义UDP通信用到的对象和变量
DatagramPacket sendPack,recePack;
DatagramSocket sendSocket,receSocket;
private InetAddress sendIp;
private int sendPort,recePort;
private byte[] inBuf;
private byte[] outBuf;
public static final int BUFSIZE=1024;
//定义一个线程,本类实现了Runnable接口,类中main主线程用于发送消息,另有一个线程负责接收消息
public Thread thread=null;
//构造函数
public UdpChat(){
//聊天界面布局定义,相对简陋,今后会尽量完善优化成QQ聊天界面的样子
//设置主窗体标题
mainJframe=new JFrame("与好友聊天中");
//获取主窗体容器
con=mainJframe.getContentPane();
//创建待发送消息文本区域和聊天记录文本区域,分别设置行数、自动换行和是否编辑
showArea=new JTextArea();
msgArea=new JTextArea();
showArea.setRows(10);
msgArea.setRows(3);
showArea.setLineWrap(true);
msgArea.setLineWrap(true);
showArea.setEditable(false);
//设置3个标签和文本模型
lab1=new JLabel("接收端口:");
lab2=new JLabel("发送端口:");
lab3=new JLabel("对方地址:");
sendPortText=new JTextField();
sendPortText.setColumns(5);
recePortText=new JTextField();
recePortText.setColumns(5);
IpAddressText=new JTextField();
IpAddressText.setColumns(8);
//创建开始和发送按钮对象
startBtn=new JButton("开始");
sendBtn=new JButton("发送");
//添加事件监听器
startBtn.addActionListener(this);
sendBtn.addActionListener(this);
//最上面的JPanel容器,存放3个标签和文本框
pane1=new JPanel();
pane1.setBackground(blue);
pane1.setLayout(new FlowLayout());
pane1.add(lab1);
pane1.add(recePortText);
pane1.add(lab2);
pane1.add(sendPortText);
pane1.add(lab3);
pane1.add(IpAddressText);
//居中的JPanel容器,存放showArea和msgArea 2个文本区域,使用边界布局
pane2=new JPanel();
pane2.setBackground(blue);
pane2.setLayout(new BorderLayout(0,20));
pane2.add(new JScrollPane(showArea),BorderLayout.NORTH);
pane2.add(new JScrollPane(msgArea),BorderLayout.CENTER);
//最下面的JPanel容器,存放开始和发送迎按钮
pane3=new JPanel();
pane3.setBackground(blue);
pane3.setLayout(new FlowLayout());
sendBtn.setSize(50, 60);
startBtn.setSize(50, 60);
pane3.add(startBtn);
pane3.add(sendBtn);
//将3个JPanel容器按照边界布局分别放到北、中、南 3个位置,充满整个窗体
con.add(pane1,BorderLayout.NORTH);
con.add(pane2,BorderLayout.CENTER);
con.add(pane3,BorderLayout.SOUTH);
mainJframe.setSize(500,400);
mainJframe.setVisible(true);
mainJframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args){
new UdpChat();
}
@Override
//开始和发送按钮监听单击事件并进行对应处理
public void actionPerformed(ActionEvent e)
{
try{
if(e.getSource()==startBtn){
//尽量将Udp通信时用到的所有变量和对象都提取出来
sendIp=InetAddress.getByName(IpAddressText.getText());
sendPort=Integer.parseInt(sendPortText.getText());
sendSocket=new DatagramSocket();
recePort=Integer.parseInt(recePortText.getText());
inBuf=new byte[BUFSIZE];
recePack=new DatagramPacket(inBuf,inBuf.length);
receSocket=new DatagramSocket(recePort);
thread=new Thread(this);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
//在msgArea中输入文本,点击发送按钮,就向对方IP和端口中发送msgArea中的数据
}else if(e.getSource()==sendBtn){
outBuf=msgArea.getText().getBytes();
sendPack=new DatagramPacket(outBuf,outBuf.length,sendIp,sendPort);
sendSocket.send(sendPack);
showArea.append("我说:"+msgArea.getText()+"\r\n");
msgArea.setText(null);
}
}
catch(UnknownHostException eh){
showArea.append("无法连接指定IP地址:"+eh.toString());
}catch(SocketException se){
showArea.append("无法连接指定端口:"+se.toString());
}catch(IOException eo){
showArea.append("发送数据失败:"+eo.toString());
}
}
@Override
public void run() {
//打循环一直接收消息
while(true){
try{
receSocket.receive(recePack);
String str=new String(recePack.getData(),0,recePack.getLength());
showArea.append("对方说:"+str+"\n");
}
catch(IOException e){
showArea.append("数据接收失败!");
}
}
}
}
运行效果如下:
TCP网络编程
同样要分别定义客户端和服务端,客户端对应的对象是Socket,服务端对应的对象是ServerSocket。
客户端定义步骤:
1. 创建Socket服务,并指定要连接的主机和端口。
2. 获取Socket流中的输出流,将数据写到流中,通过网络发送给服务端;
3. 获取Socket流中的输入流,将服务端反馈的数据获取到,并打印。
4. 关闭Socket服务,对应的输出流和输入流也随之关闭。
服务端定义步骤:
1. 建立服务端的socket服务,并监听一个端口。
2. 获取连接过来的客户端对象。
通过ServerSocket的accept方法,没有连接就会等待,是阻塞式方法。
3. 客户端如果发过来数据,那么服务端要使用对应的客户端对象并获取该客户端对象的读取流来读取发过来的数据。
4. 获取客户端对象的输出流,向客户端发送数据。
5. 关闭服务端服务(可选)。
下面是客户端和服务端交互的一个示例:
/*
需求:客户端不断向服务端发送数据,服务端收到后将数据转成大写发回给客户端,直接接收到over结束标记。
*/
import java.net.*;
import java.io.*;
public class TcpClient {
public static void main(String[] args) throws Exception
{
//创建客户端Socket服务,指定目的地址和端口
Socket s=new Socket("192.168.0.102",20007);
//定义读取键盘数据的流对象
BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in));
//定义目的,将数据写入socket输出流,发给服务端
BufferedWriter bufOut=new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
//定义一个socket读取流,读取服务端返回的大写信息
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null){
if("over".equals("line")){//键盘录入只能通过ctrl+c或定义结束标志来终止
break;
}
bufOut.write(line);
//BufferedReader类的readLine()方法不会读取任务行结束符,所以此处一定要手动换行,否则服务端中的readLine()读取不到行结束标记会一直等待。
bufOut.newLine();
//刷新缓冲区
bufOut.flush();
//读取服务端反馈信息并打印
String str=bufIn.readLine();
System.out.println("server : "+str);
}
bufr.close();
s.close();
}
}
class TcpServer{
public static void main(String[] args) throws Exception
{
//创建服务端socket服务,并监听一个端口
ServerSocket ss=new ServerSocket(20007);
//通过accept方法获取连接过来的客户端对象
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+".....connected!");
//读取socket输入流中的数据
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
//socket输出流,将大写数据写入到socket输出流,发给客户端
BufferedWriter bufOut=new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
String line=null;
while((line=bufIn.readLine())!=null){
System.out.println(line);
bufOut.write(line.toUpperCase());
bufOut.flush();
}
//关闭客户端和服务端服务
s.close();
ss.close();
}
}
客户端向服务端上传文件的例子:
/*
需求:客户端向服务端上传文件,上传成功后,服务端返回“上传成功”信息。
*/
import java.net.*;
import java.io.*;
class TextClient {
public static void main(String[] args) throws Exception
{
Socket s=new Socket("192.168.0.102",20007);
BufferedReader bufr=new BufferedReader(new FileReader("test.java"));
PrintWriter out=new PrintWriter(s.getOutputStream(),true);
//客户端发送完数据,又在等待读取服务器的返回数据,然后才关闭服务s.close(), 这样客户端会在等;而服务器也在等待读取客户端发送的数据,所以导致两端都在等,因此客户端发送完数据后,需要加结束标记,告诉服务端文件发送流已完成。
//结束标记可以是"over"等特殊字符串,但为人避免结束标记与上传文件内容冲突,最好是使用时间戳标记。
//定义时间戳标记,用来标识上传文件已结束,将该标记发给服务端,服务端在文件中读到该时间戳时就结束读取文件。
//使用可以操作基本数据类型的流对象,向服务端发送时间戳。
DataOutputStream dos=new DataOutputStream(s.getOutputStream());
long time=System.currentTimeMillis();
dos.writeLong(time);
String line=null;
while((line=bufr.readLine())!=null){
out.println(line);
}
//即使是加时间戳标记,也使代码中流对象变多,程序不简洁且麻烦;最好的办法是直接关闭输出流
//s.shutdownOutput(); 关闭客户端的输出流,相当于给流加一个结束标记,服务端输入流读取数据时不用另外判断结标记。
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
String str=bufIn.readLine();
System.out.println("server : "+str);
bufr.close();
s.close();//socket服务关闭时,才在客户端输出流(从而在服务端输入流)写入结束标记,表示发送流已完毕。
}
}
class TextServer{
public static void main(String[] args) throws Exception
{
ServerSocket ss=new ServerSocket(20007);
Socket s=ss.accept();
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+".....connected!");
//使用DataInputStream流对象接收时间戳标记
DataInputStream dis=new DataInputStream(s.getInputStream());
long time=dis.readLong();
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
PrintWriter out=new PrintWriter(new FileWriter("server.txt"),true);
String line=null;
while((line=bufIn.readLine())!=null){
//服务端在文件中读取到事先接收到的时间戳标记时就停止读取,避免了客户端和服务端都在等待的情况。
if((time+"").equals("line")){
break;
}
out.println(line);
}
PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
pw.println("上传成功!");
out.close();
s.close();
ss.close();
}
}
上述代码 可以进一步优化,实现多个客户端并发向服务端上传文件:
/*
需求:客户端并发向服务端上传文件
为了让多个客户端同时并发访问服务端,将每个客户端封装到一个单独的线程中。
定义 线程时,将每个客户端要在服务端执行的代码,放到run()方法即可。
*/
import java.io.*;
import java.net.*;
public class TextThread implements Runnable
{
private Socket s;
TextThread(Socket s){
this.s=s;
}
public static void main(String[] args) {
}
@Override
public void run() {
int count=1;
String ip=s.getInetAddress().getHostAddress();
try{
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
//使用不同的文件名存储客户端上传上来的文件
File file=new File(ip+"("+count+")"+".txt");
if(file.exists())
file=new File(ip+"("+(count++)+")"+".txt");
PrintWriter out=new PrintWriter(new FileWriter(file),true);
String line=null;
while((line=bufIn.readLine())!=null){
out.println(line);
}
PrintWriter pw=new PrintWriter(s.getOutputStream(),true);
pw.println("上传成功!");
out.close();
s.close();
}
catch(Exception e){
throw new RuntimeException(ip+"上传失败!");
}
}
}
//客户端代码可以不变,服务端代码变为:
class TextServer2{
public static void main(String[] args) throws Exception
{
ServerSocket ss=new ServerSocket(20007);
while(true){
Socket s=ss.accept();
new Thread(new TextThread(s)).start();
}
}
}
浏览器客户端和服务端
前面写了几个客户端和服务端程序,实际上,访问上面的服务端时,可以直接在IE浏览器访问服务端代码所在机器的IP地址和端口号,即浏览器可以作为客户端,与服务器进行交互。
Tomcat 服务器里面就封装了ServerSocket。在服务端程序中获取并打印客户端输入流s.getInputStream(),可以看到客户端向服务端发送的http请求信息,打印服务端输出流可以看到服务端向客户端发送的http响应。
URL
统一资源定位符,继承自Object类,常用构造方法:
URL(String url), 根据String表示形式创建URL对象。
URL(String protocol, String host, int port, String file),根据指定protocol、host、port号和File 创建URL对象。
URL(String protocol, String host, String file),根据指定protocol、host、port号和File 创建URL对象。
常用方法在下面代码中演示及说明:
/*
启动192.168.0.102主机上的tomcat服务器,运行此程序,可以看到tomcat服务器上的网页demo.html
*/
public class URLDemo{
public static void main(String[]args){
String urlPath="http://192.168.0.102:8080/myweb/demo.html" ;
URL url=new URL(urlPath);//创建URL对象,然后可以URL类的方法获取各个部分。
sop(getFile());//获取此URL的文件名
sop(getHost());//获取此URL的主机名,
sop(getPort());//获取此URL的端口,如果url对象创建时没有指定端口,方法返回-1
sop(getPath());//获取此URL的路径部分
sop(getQuery());//获取此URL的查询部分
//连接url对象中的主机、端口,并访问其文件
URLConnection conn=rul.openConnection();
//获取URLConnection的输入流,打印Tomcat服务器上url所代表的资源数据;也可以获取输出流getOutputStream
InputStream in=conn.getInputStream();
//上面2句代码可用url.openInputStream()来代替
byte[] buf=new byte[1024];
int len=in.read(buf);
//不显示http消息响应头,只显示消息体,因为URLConnecton可以解析发送过来的http消息,只返回消息内容。
sop(new String(buf,0,len));
}
public static void sop(Object obj){
System.out.println(obj);
}
}