浅谈Java分布式计算
浅谈Java分布式计算
存取数据库怎么办 ?
我们知道 , 大多数情况下 , 方法的调用都是发生在相同堆上的两个对象之间 , 如果要调用不同机器上的对象的方法呢 ?
通常 , 我们从某一台计算机上面取得另一台计算机上的信息是通过 socket 的输入 / 输出流 , 打开另一台计算机的 socket 连接 , 然后取得 outputStream 来写入数据 . 但如果要调用另一台计算机上 , 另一个 Java 虚拟机上面的对象的方法你?我们当然可以自己定义和设计通信协议来调用 , 然后通过 Socket 把执行结果再传回去 , 并且还能够像是对本机的方法调用一样 , 也就是说想要调用远程的对象 ( 像是别的堆上的 ), 却又要像是一般的调用 .
这就是 RMI 带给我们的功能 .
远程过程调用的设计
要创建出 4 种东西 : 服务器、客户端、服务器辅助设施和客户端辅助设施 .
1. 创建客户端和服务端应用程序 , 服务器应用程序时个远程服务 , 是个带有客户端会调用的方法的对象
2. 创建客户端和服务器端的辅助设施 (helper) 他们会处理所有客户端和服务器的底层网络输入 / 输出细节 , 让客户端和程序好像在处理本地调用一样 .
辅助设施的任务 辅助设施是个在实际上执行通信的对象,他们会让客户端感觉上好像是在调用本机对象,客户端对象看起来像是在调用远程的方法,但实际上它只是在调用本地处理Socket和串流细节的代理.在服务器这端,服务器的辅助设施会通过socket连接来自客户端设施的要求,解析打包送来的信息,然后调用真正的服务,因此对服务对象来说此调用来自本地.服务的辅助设施取得返回值之后就把它包装然后送回去(通过socket的输出串流)给客户端的辅助设施.客户端的辅助设施会解开这些信息传输给客户端的对象
调用方法的过程
1. 客户端对象对辅助设施对象调用 doBigThing()
2. 客户端辅助设施把调用信息打包通过网络送到服务器的辅助设施
3. 服务端的辅助设施解开来自客户端辅助设施的信息 , 并以此调用真正的服务 .
这个过程的描述图如下 :
Java RMI 提供客户端和服务器端的辅助设施对象
在 Java 中 ,RMI 已经帮我们创建好客户端和服务器端的辅助设施 , 它也知道如何让客户端辅助设施看起来像是真正的服务 , 也就是说 ,RMI 知道如何提供相同的方法给客户端调用 .
此外 ,RMI 有提供执行期所需全部的基础设施 , 包括服务的查询以及让客户端能够找到与取得客户端的辅助设施 ( 真正的服务代理人 ).
使用 RMI 时 , 无需编写任何网络或输入 / 输出的程序 , 客户端对远程方法的调用就跟对同一个 Java 虚拟机上的方法调用是一样的 .
一般调用和 RMI 调用有一点不同 , 虽然对客户端来说 , 此方法调用看起来像是本地的 , 但是客户端辅助设施会通过网络发出调用 , 此调用最终还是会涉及到 socket 和串流 , 一开始是本机调用 , 代理会把它转成远程的 . 中间的信息是如何从 Java 虚拟机送到 Java 虚拟机要看辅助设施对象所用的协议而定 .
使用 RMI 时 , 必须要决定协议: JRMP 或 IIOP,JRMP 是 RMI 原生的协议 , 它是为 Java 间的远程调用而设计的 , 另外一方面 ,IIOP 是为了 CORBA 而产生的 , 它让我们能够调用 Java 对象或其它类型的远程方法 ,CORBA 通常比 RMI 麻烦 , 因为若两端不全都是 Java 的话 , 就会产生一堆可怕的转译和交谈操作 .
我们只关心 Java 对 Java 的操作,所以会使用相当简易的 RMI.
在 RMI 中 , 客户端的辅助设施称为 stub, 而服务器端的辅助设施称为 skeleton.
如何创建远程服务
1. 创建 Remote 接口
远程的接口定义了客户端可以远程调用的方法 , 它是个作为服务的多态化类 .stub 和服务都会实现此接口
2. 实现 Remote 接口
这个是真正执行的类 , 它实现出定义在该接口上的方法 , 它是客户端会调用的对象
3. 用 rmic 产生 stub 和 skeleton
客户端和服务器都有 helper, 我们无需创建这些类或产生这些类的源代码 , 这都会在执行 JDK 所附的 rmic 工具时自动地处理掉
4. 启动 RMI registry (rmiregistry)
rmiregistry 就像电话薄 , 用户会从此处取得代理 ( 客户端的 stub/helper 对象 )
5. 启动远程服务
必须让服务对象开始执行 , 实现服务的类会起始服务的实例并向 RMI Registry 注册 , 要有注册后才能对用户服务 .
服务端代码
定义接口
import java.rmi.Remote; import java.rmi.RemoteException; /** * * MyRemote.java * * 功 能: TODO * 类 名: MyRemote.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-3-19 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * * * Remote是个标记性的接口,意味着没有方法,然而它对RMI有特殊的意义,所以必须遵守这项规则, * 注意这里用的是extends,接口是可以继承其他接口的 * */ public interface MyRemote extends Remote{ /** * 远程的接口定义了客户端可以远程调用的方法,它是作为服务的多态化类,也就是说,客户端会 * 调动有实现此接口的stub,而此stub因为会执行网络和输入/输出工作,所以可能会发生各种 * 问题,客户端鼻息处理或声明异常来认知这一类风险,如果该方法在接口中声明异常,调用该方 * 法的所有程序都必须处理或再声明此异常. * * 远程方法的参数和返回值必须是primitive或serializable的.任何远程方法的参数都会被 * 打包通过网络传送,而这时通过序列化完成的,返回值也是一样.所以,如果使用的是自定义类型 * 时,必须对其序列化 * @return * @throws RemoteException * 所有接口中的方法都必须声明RemoteException */ public String sayHello() throws RemoteException; }
业务实现
import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; /** * * MyRemoteImpl.java * * 功 能: TODO * 类 名: MyRemoteImpl.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-3-19 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * * 为了要成为远程服务对象,对象必须要有与远程有关的功能,其中最简单的方法就是继承UnicastRemoteObject * (来自java.rmi.server)以让这个父类处理这些工作 * */ public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote{ /** * 父类的构造函数声明了异常,所有你必须写出构造函数,因为它代表你的构造函数会调用有风险的程序代码 * * UnicastRemoteObject有个小问题,它的构造函数会抛出RemoteException.处理它的唯一方式就是 * 对自己的实现声明一个构造,如此才会有地方可以声明出RemoteException.当类被初始化的时候,父类 * 的构造函数一定会被调用,如果父类的构造函数抛出异常,我们也必须声明的自定义的构造函数会抛出异常 * @throws RemoteException */ protected MyRemoteImpl() throws RemoteException { } /** * 实现出接口所有的方法,但无需声明RemoteException */ @Override public String sayHello(){ return "server says, rmi hello world !" ; } public static void main(String[] args) { try { /** * 我们已经有了远程服务,还必须要让远程用户存取,这可以通过将它初始化并加进RMI Registry * (它一定要运行起来,不然此程序就会失败).当注册对象时,RMI系统会把stub加到registry中, * 因为这是客户端所需要的.使用java.rmi.Naming的rebind()来注册服务 */ MyRemote service = new MyRemoteImpl(); /** * 创建出远程对象,然后使用静态的Naming.rebind()来产生关联,所注册的名称会提供客户端查询 */ Naming.rebind( "Remote Hello World" , service); } catch (Exception e) { e.printStackTrace(); } } }
客户端代码
import java.rmi.Naming; /** * * MyRemoteClient.java * * 功 能: TODO * 类 名: MyRemoteClient.java * * ver 変更日 角色 担当者 変更内容 * ────────────────────────────────────────────── * V1.00 2013-3-19 模块 苏若年 初版 * * Copyright (c) 2013 dennisit corporation All Rights Reserved. * * Email:<a href="mailto:DennisIT@163.com">发送邮件</a> * */ public class MyRemoteClient { public void exec(){ try { /** * 客户端必须取得stub对象,因为客户端必须要调用它的方法.这就得靠RMI registry了.客户端会像查询电话 * 簿一样地搜索,找出上面有相符的名称的服务. * 客户端查询RMIRegistry,返回stub对象 * Naming.lookup("rmi://127.0.0.1/Remote Hello World"); * 参数说明 * rmi://127.0.0.1/Remote Hello World * 127.0.0.1表示主机名称或主机IP地址 * Remote Hello World必须要跟注册的名称一样 * */ MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/Remote Hello World" ); String tmp = service.sayHello(); System.out.println(tmp); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { new MyRemoteClient().exec(); } }
对实现出的类 ( 不是 remote 接口 ) 执行 rmic
伴随 JDK 而来的 rmic 工具会以服务的实现产生 2 个心的类 stub 和 skeleton. 它会按照命名规则在远程实现名称后面加上 _Stub 或 _Skeleton 。 rmic 有几个选项 , 包括了不产生 skeleton 、观察产生出类的源代码或使用 IIOP 作为通讯协议等 . 产生出的类会放在当前目录下 , 要记住 rmic 必须能够找到所实现的类 , 因此可能要从实现所在的目录执行 rmic( 实际中可能需要考虑到包目录结构和完整名称 , 为了简便这里没有运用到包 )
调用命令行来启动 rmiregistry, 要确定是从可以存取到该类的目录来启动 , 最简单的方法就是从类这个目录来运行 .
运行截图如下
注意 :
客户端是使用接口来调用 stub 上的方法 , 客户端的 Java 虚拟机必须要有 stub 类 , 但客户端不会在程序代码中引用到 stub 类 , 客户端总是通过接口来操作真正的远程对象
服务器上必须要有 stub 和 skeleton, 以及服务与远程的接口 , 它会需要 stub 类是因为 stub 会被代换成连接在 RMIRegistry 上真正的服务 .
使用 RMI 时常犯的错误 :
1. 忘记在启动远程服务钱启动 rmiregistry( 使用 Naming.rebind() 注册服务前 rmiregistry 必须启动 )
2. 忘记把参数和返回类型做成可序列化 ( 编译不会检测到 , 执行时才会发现 )
3. 忘记将 stub 类交给客户端
RMI 很适合编写并运行远程服务 , 但我们不会单独使用 RMI 来执行网站服务 , 对大型的企业级应用程序来说 , 我们需要更多更好的功能 . 像交易管理、大量并发处理、安全性和数据库管理等 . 这就需要用到 Enterprise Application Server.
JavaEE 服务器包括了 Web 服务器和 Enterprise JavaBeans(EJB) 服务器 . EJB 服务器作用于 RMI 调用和服务层之间 .
RMI 在 JINI 中的应用
Jini 也是使用 RMI( 虽然也可以用别的协议 ), 但多了几个关键功能 .
1. 自适应探索 (adaptive discovery)
2. 自恢复网络 (self-healing networks)
RMI 的客户端得先取得远程服务的地址和名称 . 客户端的查询程序代码就要带有远程服务的 IP 地址或主机名 ( 因为 RMIRegistry 就在上面 ) 以及服务所注册的名称
但是用 JINI 时 , 用户只需要知道一件事 , 服务所实现的接口 ! 这样就行 .
Jini 是用 lookup service, 该查询服务比 RMI Registry 更强更有适应性 . 因为 Jini 会在网络上自动的广告 . 当查询服务上线是 , 它会使用 IP 组播技术送出信息给整个网络 . 不止这样 , 如果客户端在查询服务已经广播之后上线 , 客户端也可以发出消息给整个网络来询问 .
当服务上线时 , 它会动态的探索网络上的 JINI 查询服务并申请注册 , 注册时 , 服务会送出一个序列化的对象给查询服务 , 此对象可以是 RMI 远程服务的 stub 、网络装置的驱动程序 , 甚或是可以在客户端执行的服务本身 . 并且注册的是所实现的接口 . 而不是名称 .
自适应探索的运作
1. Jini 查询服务在网络上启动 , 并使用 IP 组播技术为自己做宣传
2. 已经启动的另外一个 Jini 服务会寻求向刚启动的查询服务注册 . 它注册的是功能而不是名称 , 也就是所实现的接口 , 然后送出序列化对象给查询服务
3. 网络客户想要取得实现 ScientificCalculator 的东西 , 可是不知道哪里有 , 所以就问查询服务
4. 查询服务响应查询的结果
自恢复网络的运作
1. 某个 Jini 服务要求注册 , 查询服务会给一份租约 , 新注册的服务必须要定期更新租约 , 不然查询服务会假设此服务已经离线了 , 查询服务会力求呈现精确完整的可用服务网络状态
2. 因为关机所以服务离线 , 因此没有更新租约 , 查询服务就把它踢掉 .
转载请注明出处:[ http://www.cnblogs.com/dennisit/archive/2013/03/19/2969175.html ]
热爱生活,热爱Coding,敢于挑战,用于探索 ...
分类: JavaWeb
标签: java , 分布式计算 , RMI , JINI , EJB
Socket编程之简单介绍 2013-03-19 15:27 by 蓝天下的雨, 461 阅读, 2 评论, 收藏 , 编辑
一:套接字编程相关知识点
Socket 概念 :套接字是一种通信机制,凭借这种机制,客户 / 服务器系统的开发工作既可以在本地单机进行,也可以跨网络进行。
网络 中的进程是通过 socket 来通信的 。 socket 起源于 Unix ,而 Unix/Linux 基本哲学之一就是 “ 一切皆文件 ” ,都可以用 “ 打开 open –> 读写 write/read –> 关闭 close” 模式来操作。我的理解就是 Socket 就是该模式的一个实现, socket 即是一种特殊的文件,一些 socket 函数就是对其进行的操作(读 / 写 IO 、打开、关闭)。
Socket 基本操作:
Socket 编程的基本函数有 :
socket(),bind(),accept(),send(),sendto(),recv() 以及 recvfrom() 等,其中根据客户端还是服务器端,或者根据使用 TCP 还是 UDP ,这些函数的调用流程都有所区别,下面对每个函数进行说明。
Socket() 函数:
int socket (int domain, int type, int protocol);
socket 函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而 socket() 用于创建一个 socket 描述符( socket descriptor ),它唯一标识一个 socket 。这个 socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
参数的意思:
d omain : 即协议域,又称为协议族( family )。常用的协议族有, AF_INET 、 AF_INET6 、 AF_LOCAL (或称 AF_UNIX , Unix 域 socket )、 AF_ROUTE 等等。协议族决定了 socket 的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址( 32 位的)与端口号( 16 位的)的组合、 AF_UNIX 决定了要用一个绝对路径名作为地址。
type :指定 socket 类型。常用的 socket 类型有 :
SOCK_STREAM :流式套接字,提供可靠的、面向连接的通信流;它使用 TCP ,从而保证了数据传输的可靠性和顺序性。
SOCK_DGRAM :数据报套接字,定义了一种无可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错的。它使用 UDP 。
SOCK_RAW :原始套接字,允许对底层协议,如 IP 或 ICMP 进行直接访问,功能比较强大,但是使用较为不便,主要用于一些协议的开发。
protocol :故名思意,就是指定协议。常用的协议有, IPPROTO_TCP 、 IPPTOTO_UDP 、 IPPROTO_SCTP 、 IPPROTO_TIPC 等,它们分别对应 TCP 传输协议、 UDP 传输协议、 STCP 传输协议、 TIPC 传输协议。
当我们调用 socket 创建一个 socket 时,返回的 socket 描述字它存在于协议族( address family , AF_XXX )空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind() 函数,否则就当调用 connect() 、 listen() 时系统会自动随机分配一个端口。
Bind() 函数:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
正如上面所说 bind() 函数把一个地址族中的特定地址赋给 socket 。例如对应 AF_INET 、 AF_INET6 就是把一个 ipv4 或 ipv6 地址和端口号组合赋给 socket 。
函数参数:
sockfd : 即 socket 描述字,它是通过 socket() 函数创建了,唯一标识一个 socket 。 bind () 函数就是将给这个描述字绑定一个名字。
addr :是 一个 const struct sockaddr * 指针,指向要绑定给 sockfd 的协议地址。这个地址结构根据地址创建 socket 时的地址协议族的不同而不同,如 ipv4 对应的如下所示:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
ipv6 对应的是:
struct sockaddr_in6 {
sa_family_t sin 6_family; /* AF_INET6 */
in_port_t sin 6_port; /* port number */
uint32_t sin 6_flowinfo; /* IPv6 flow information */
struct in6_addr sin 6_addr; /* IPv6 address */
uint32_t sin 6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
addrlen :对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip 地址 + 端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的 ip 地址组合。这就是为什么通常服务器端在 listen 之前会调用 bind() ,而客户端就不会调用,而是在 connect() 时由系统随机生成一个。
listen() 、 connect() 函数
如果作为一个服务器,在调用 socket() 、 bind() 之后就会调用 listen() 来监听这个 socket ,如果客户端这时调用 connect() 发出连接请求,服务器端就会接收到这个请求 。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen 函数的第一个参数即为要监听的 socket 描述字,第二个参数为相应 socket 可以排队的最大连接个数。 socket() 函数创建的 socket 默认是一个主动类型的, listen 函数将 socket 变为被动类型的,等待客户的连接请求。
connect 函数的第一个参数即为客户端的 socket 描述字,第二参数为服务器的 socket 地址,第三个参数为 socket 地址的长度。客户端通过调用 connect 函数来建立与 TCP 服务器的连接。
accept() 函数
TCP 服务器端依次调用 socket() 、 bind() 、 listen() 之后,就会监听指定的 socket 地址了。 TCP 客户端依次调用 socket() 、 connect() 之后就向 TCP 服务器发送了一个连接请求。 TCP 服务器监听到这个请求之后,就会调用 accept() 函数接收请求,这样连接就建立好了。之后就可以开始网络 I/O 操作了,即类同于普通文件的读写 I/O 操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept 函数的第一个参数为服务器的 socket 描述字,第二个参数为指向 struct sockaddr * 的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果 accpet 成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接。
注意 : accept 的第一个参数为服务器的 socket 描述字,是服务器开始调用 socket() 函数生成的,称为监听 socket 描述字;而 accept 函数返回的是已连接的 socket 描述字。一个服务器通常仅仅只创建一个监听 socket 描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接 socket 描述字,当服务器完成了对某个客户的服务,相应的已连接 socket 描述字就被关闭。
read() 、 write() 等函数
至此服务器与客户已经建立好连接了。可以调用网络 I/O 进行读写操作了,即实现了网咯中不同进程之间的通信!网络 I/O 操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
推荐使用 recvmsg()/sendmsg() 函数,这两个函数是最通用的 I/O 函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read 函数是负责从 fd 中读取内容 . 当读成功时, read 返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。如果错误为 EINTR 说明读是由中断引起的,如果是 ECONNREST 表示网络连接出了问题。
write 函数将 buf 中的 nbytes 字节内容写入文件描述符 fd. 成功时返回写的字节数。失败时返回 -1 ,并设置 errno 变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。 1)write 的返回值大于 0 ,表示写了部分或者是全部的数据。 2) 返回的值小于 0 ,此时出现了错误。我们要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断错误。如果为 EPIPE 表示网络连接出现了问题 ( 对方已经关闭了连接 ) 。
Close() 函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的 socket 描述字,好比操作完打开的文件要调用 fclose 关闭打开的文件。
#include <unistd.h>
int close(int fd);
close 一个 TCP socket 的缺省行为时把该 socket 标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为 read 或 write 的第一个参数。
注意 : close 操作只是使相应 socket 描述字的引用计数 -1 ,只有当引用计数为 0 的时候,才会触发 TCP 客户端向服务器发送终止连接请求。
socket 中 TCP 的三次握手建立连接详解
大致流程如下:
客户端向服务器发送一个 SYN J 。
服务器向客户端响应一个 SYN K ,并对 SYN J 进行确认 ACK J+1 。
客户端再想服务器发一个确认 ACK K+1 。
三次握手与 socket 编程之中的函数对应关系如下图所示:
从图中可以看出,当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K , ACK J+1 ,这时 accept 进入阻塞状态;客户端收到服务器的 SYN K , ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;服务器收到 ACK K+1 时, accept 返回,至此三次握手完毕,连接建立。
( 3 )网络字节序和主机字节序
主机字节序 就是我们平常说的大端和小端模式:不同的 CPU 有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的 Big-Endian 和 Little-Endian 的定义如下:
a) Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序 : 4 个字节的 32 bit 值以下面的次序传输:首先是 0 ~ 7bit ,其次 8 ~ 15bit ,然后 16 ~ 23bit ,最后是 24~31bit 。这种传输次序称作大端字节序。 由于 TCP/IP 首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以:在将一个地址绑定到 socket 的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是 Big-Endian 。所以对主机字节序,务必将其转化为网络字节序后再赋给 socket 。
简单实例:
客户端client.c
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main()
{
int sockfd,sendbytes;
char buf[BUFFER_SIZE];
struct sockaddr_in serv_addr;
/* 创建 socket*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(1);
}
/* 设置 sockaddr_in 结构体中相关参数 */
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(serv_addr.sin_zero),8);
/* 调用 connect 函数主动发起对服务器端的连接 */
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))==-1)
{
perror("send");
exit(1);
}
fgets(buf,BUFFER_SIZE,stdin);
}
服务器端:
server.c
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[BUFFER_SIZE];
/* 建立 socket 连接 */
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d \n",sockfd);
/* 设置 sockaddr_in 结构体中相关参数 */
server_sockaddr.sin_family =AF_INET; // IPv4
server_sockaddr.sin_port =htons(PORT); // 主机字节序转为网络字节序
server_sockaddr.sin_addr.s_addr = INADDR_ANY; // 任意地址
bzero(&(server_sockaddr.sin_zero),8);
int i=1; // 允许重复使用本地地址与套接字进行绑定
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));
/* 绑定操作 */
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))==-1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
/* 调用 listen 函数创建未处理请求的队列 */
if(listen(sockfd,MAX_QUE_CONN_NM)==-1)
{
perror("listen");
exit(1);
}
printf("Listening...\n");
/* 调用 accept 函数,等待客户端的连接 */
sin_size = sizeof(client_sockaddr);
if((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
/* 调用 rec v 函数接收客户端的请求 */
memset(buf,0,sizeof(buf));
if((recvbytes = recv(client_fd,buf,BUFFER_SIZE,0)) == -1)
{
perror("recv");
exit(1);
}
printf("Server receives a message:%s \n",buf);
close(sockfd);
exit(0);
}
分类: 网络编程
作者: Leo_wl
出处: http://www.cnblogs.com/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息