socket通信

系统 1724 0

1.Soket 发展史以及它和 tcp/ip 的关系

七十年代中,美国国防部高研署 (DARPA) TCP/IP 的软件提供给加利福尼亚大学 Berkeley 分校后, TCP/IP 很快被集成到 Unix 中,同时出现了许多成熟的 TCP/IP 应用程序接口 (API) 。这个 API 称为 Socket 接口。今天, SOCKET 接口是 TCP/IP 网络最为 通用的 API ,也是在 INTERNET 上进行应用开发最为通用的 API
  九十年代初,由 Microsoft 联合了其他几家公司共同制定了一套 WINDOWS 下的网络编程接口,即 Windows Sockets 规范。它是 Berkeley Sockets 的重要扩充,主要是增加了一些异步函数,并增加了符合 Windows 消息驱动特性的网络事件异步选择机制。 Windows Sockets 规范是一套开放的、支持多种协议的 Windows 下的网络编程接口。目前,在实际应用中的 Windows Sockets 规范主要有 1.1 版和 2.0 版。两者的最重要区别是 1.1 版只支持 TCP/IP 协议,而 2.0 版可以支持多协议, 2.0 版有良好的向后兼容 性,目前, Windows 下的 Internet 软件都是基于 WinSock 开发的。

Socket 实际在计算机中提供了一个通信端口,可以通过这个端口与任何一个具有 Socket 接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个 Socket 接口来实现。在应用开发中就像使用文件 句柄一样,可以对 Socket 句柄进行读、写操作。套接字是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。套接字存在通信区域(通信区域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。 WINDOWS 中的套接字只支持一个域 —— 网际域。套接字具有类型。我们将 Socket 翻译为套接字,套接字分为以下三种类型:
  字节流套接字 (Stream Socket)  是最常用的套接字类型, TCP/IP 协议族中的 TCP 协议使用此类接口。字节流套接口提供面向连接的 ( 建立虚电路 ) 、无差错的、发送先后顺序一致的、无记录边界和非重复的网络信包传输。
数据报套接字 (Datagram Socket) TCP/IP 协议族中的 UDP 协议使用此类接口,它是无连接的服务,它以独立的信包进行网络传输,信包最大长度为 32KB ,传输不保证顺 序性、可靠性和无重复性,它通常用于单个报文传输或可靠性不重要的场合。数据报套接口的一个重要特点是它保留了记录边界。对于这一特点。数据报套接口采用了与现在许多包交换网络 ( 例如以太网 ) 非常类似的模型。
  原始数据报套接字 (Raw Socket)  提供对网络下层通讯协议 ( IP 协议 ) 的直接访问,它一般不是提供给普通用户的,主要用于开发新的协议或用于提取协议较隐蔽的功能。

2 socket 通信概念

* 端口
网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照 OSI 七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。 从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此, TCP/IP 协议提出了协议端口( protocol port ,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和 I/O 缓冲区)。应用程序(即进程)通过系统调 用与某端口建立连接( binding )后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在 TCP/IP 协议的实现中,端口操作类似于一般的 I/O 操作,进程获取一个端口,相当于获取本地唯一的 I/O 文件,可以用一般的读写原语访问之。
类似于文件描述符,每个端口都拥有一个叫端口号( port number )的整数型标识符,用于区别不同端口。由于 TCP/IP 传输层的两个协议 TCP UDP 是完全独立的两个软件模块,因此各自的端口号也相互独立,如 TCP 有一个 255 号端口, UDP 也可以有一个 255 号端口,二者并不冲突。

* 地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接。因此需要三级寻址:

某一主机可与多个网络相连,必须指定一特定网络地址;

网络上每一台主机应有其唯一的地址;

每一主机上的每一进程应有在该主机上的唯一标识符。
通常主机地址由网络 ID 和主机 ID 组成,在 TCP/IP 协议中用 32 位整数值表示; TCP UDP 均使用 16 位端口号标识用户进程。

* 网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。 TCP/IP 协议使用 16 位整数和 32 位整数的高价先存格式,它们均含在协议头文件中。

* 面向连接

可靠的报文流、可靠的字节流、可靠的连接,如:文件传输( FTP )、远程登录( Telnet
数字话音。

* 无连接

不可靠的数据报、有确认的数据报、请求-应答,如:电子邮件( E-mail )、电子邮件中的挂号信、网络数据库查询。

* 顺序

在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序

可能会与发送时不同。 " 顺序 " 是指接收数据顺序与发送数据顺序相同。 TCP 协议提供这项服务。

* 差错控制

保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验、检查和 Checksum 的方法。而保证传送无差错的方法是双方采用确认应答技术。 TCP 协议提供这项服务。

* 流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。 TCP 协议提供这项服务。

* 字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。 TCP 协议提供字节流服务。

* 报文
接收方要保存发送方的报文边界。 UDP 协议提供报文服务。

* 全双工 / 半双工
端-端间数据同时以两个方向 / 一个方向传送。

* 缓存 / 带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。

* 客户 / 服务器模式

socket通信

TCP/IP 网络应用中,通信的两个进程间相互作用的主要模式是客户 / 服务器模式( Client/Server model ),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户 / 服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户 / 服务器模式的 TCP/IP
客户 / 服务器模式在操作过程中采取的是主动请求方式:
首先服务器方要先启动,并根据请求提供相应服务:

打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如 FTP 21 )接收客户请求;

等待客户请求到达该端口;

接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如 UNIX 系统中用 fork exec )。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。

返回第二步,等待另一客户请求。

关闭服务器

客户方:

打开一通信通道,并连接到服务器所在主机的特定端口;

向服务器发服务请求报文,等待并接收应答;继续提出请求 ......

请求结束后关闭通信通道并终止。

从上面所描述过程可知:

客户与服务器进程的作用是非对称的,因此编码不同。

服务进程一般是先于客户请求而启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。

3.socket 通信五元组

* SOCKET PASCAL FAR socket(int af, int type, int protocol)
该调用要接收三个参数: af type protocol 。参数 af 指定通信发生的区域, UNIX 系统支持的地址族有: AF_UNIX AF_INET AF_NS 等,而 DOS WINDOWS 中仅支持 AF_INET ,它是网际网区域。因此,地址族与协议族相同。参数 type 描述要建立的套接字的类型。参数 protocol 说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为 0 ,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此, socket() 系统调用实际上指定了相关五元组中的 " 协议 " 这一元。

TCP/IP socket 提供下列三种类型套接字。

流式套接字( SOCK_STREAM
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议( FTP )即使用流式套接字。

数据报式套接字( SOCK_DGRAM
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统( NFS )使用数据报式套接字。

原始式套接字( SOCK_RAW
该接口允许对较低层协议,如 IP ICMP 直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。

* int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen)
参数 s 是由 socket() 调用返回的并且未作连接的套接字描述符 ( 套接字号 ) 。参数 name 是赋给套接字 s 的本地地址(名字),其长度可变,结构随通信域的不同而不同。 namelen 表明了 name 的长度。

* int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数 s 是欲建立连接的本地套接字描述符。参数 name 指出说明对方套接字地址结构的指针。对方套接字地址长度由 namelen 说明。

* SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数 s 为本地套接字描述符,在用做 accept() 调用的参数前应该先调用过 listen() addr 指向客户方套接字地址结构的指针,用来接收连接实体的地址。 addr 的确切格式由套接字创建时建立的地址族决定。 addrlen 为客户方套接字地址的长度(字节数)。如果没有错误发生, accept() 返回一个 SOCKET 类型的值,表示接收到的套接字的描述符。否则返回值 INVALID_SOCKET

调用 accept() 后,服务器等待从编号为 s 的套接字上接受客户连接请求,而连接请求是由客户方的 connect() 调用发出的。当有连接请求到达时, accept() 调用将请求连接队列上的第一个客户方套接字地址及长度放入 addr addrlen ,并创建一个与 s 有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。

监听连接 ── listen()
此调用用于面向连接服务器,表明它愿意接收连接。 listen() 需在 accept() 之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数 s 标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。 backlog 表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为 5 。如果没有错误发生, listen() 返回 0 。否则它返回 SOCKET_ERROR listen() 在执行调用过程中可为没有调用过 bind() 的套接字 s 完成所必须的连接,并建立长度为 backlog 的请求连接队列。
调用 listen() 是服务器接收一个连接请求的四个步骤中的第三步。它在调用 socket() 分配一个流套接字,且调用 bind() s 赋于一个名字之后调用,而且一定要在 accept() 之前调用。

recv() 调用用于在参数 s 指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数 s 为已连接的套接字描述符。 buf 指向接收输入数据缓冲区的指针,其长度由 len 指定。 flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生, recv() 返回总共接收的字节数。如果连接被关闭,返回 0 。否则它返回 SOCKET_ERROR

send() 调用用于在参数 s 指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数 s 为已连接的本地套接字描述符。 buf 指向存有发送数据的缓冲区的指针,其长度由 len 指定。 flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生, send() 返回总共发送的字节数。否则它返回 SOCKET_ERROR

输入 / 输出多路复用 ── select()
select()
调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个 fd_set 结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select() 调用返回满足条件的套接字的数目,其调用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
参数 nfds 指明被检查的套接字描述符的值域,此变量一般被忽略。
参数 readfds 指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数 writefds 指向要做写检测的套接字描述符集合的指针。 exceptfds 指向要检测是否出错的套接字描述符集合的指针。 timeout 指向 select() 函数等待的最大时间,如果设为 NULL 则为阻塞操作。 select() 返回包含在 fd_set 结构中已准备好的套接字描述符的总数目,或者是发生错误则返回 SOCKET_ERROR

select() 的机制中提供一 fd_set 的数据结构,实际上是一 long 类型的数组,每一个数组元素都能与一打开的文件句柄(不管是 Socket 句柄 , 还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用 select() 时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了 select() 的进程哪一 Socket 或文件可读,下面具体解释:
#include<sys/types.h>
#include<sys/times.h>
#include<sys/select.h>

intselect(nfds,readfds,writefds,exceptfds,timeout)
intnfds;
fd_set*readfds,*writefds,*exceptfds;
structtimeval*timeout;

ndfs
select 监视的文件句柄数,视进程中打开的文件数而定 , 一般设为呢要监视各文件中的最大文件号加一。
readfds
select 监视的可读文件句柄集合。
writefds:select
监视的可写文件句柄集合。
exceptfds
select 监视的异常文件句柄集合。
timeout
:本次 select() 的超时结束时间。(见 /usr/sys/select.h ,可精确至百万分之一秒!)

readfds writefds 中映象的文件可读或可写或超时,本次 select()
就结束返回。程序员利用一组系统提供的宏在 select() 结束时便可判
断哪一文件可读或可写。对 Socket 编程特别有用的就是 readfds
几只相关的宏解释如下:

FD_ZERO(fd_set*fdset)
:清空 fdset 与所有文件句柄的联系。
FD_SET(intfd,fd_set*fdset)
:建立文件句柄 fd fdset 的联系。
FD_CLR(intfd,fd_set*fdset)
:清除文件句柄 fd fdset 的联系。
FD_ISSET(intfd,fdset*fdset)
:检查 fdset 联系的文件句柄 fd 是否可读写, >0 表示可读写。
(关于 fd_set 及相关宏的定义见 /usr/include/sys/types.h

这样,你的 socket 只需在有东东读的时候才读入,大致如下:

...
intsockfd;
fd_setfdR;
structtimevaltimeout=..;
...
for(;;){
FD_ZERO(&fdR);
FD_SET(sockfd,&fdR);
switch(select(sockfd+1,&fdR,NULL,&timeout)){
case-1:
errorhandledbyu;
case0:
timeouthanledbyu;
default:
if(FD_ISSET(sockfd)){
nowureadorrecvsomething;
/*ifsockfdisfatherand
serversocket,ucannow
accept()*/
}
}
}

所以一个 FD_ISSET(sockfd) 就相当通知了 sockfd 可读。 至于 structtimeval 在此的功能,请 manselect 。不同的 timeval 设置使使 select() 表现出超时结束、无超时阻塞和轮询三种特性。由于 timeval 可精确至百万分之一秒,所以 Windows SetTimer() 根本不算什么。你可以用 select() 做一个超级时钟。

服务器方程序:
/* File Name: streams.c */
#include
#include
#define TRUE 1
/*
这个程序建立一个套接字,然后开始无限循环;每当它通过循环接收到一个连接,则打印出一个信息。当连接断开,或接收到终止信息,则此连接结束,程序再接收一个新的连接。命令行的格式是: streams */
main( )
{
int sock, length;
struct sockaddr_in server;
struct sockaddr tcpaddr;
int msgsock;
char buf[1024];
int rval, len;
/*
建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/*
使用任意端口命名套接字 */
server.sin_family = AF_INET;
server.sin_port = INADDR_ANY;
if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
perror("binding stream socket");
exit(1);
}
/*
找出指定的端口号并打印出来 */
length = sizeof(server);
if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
perror("getting socket name");
exit(1);
}
printf("socket port #%d/n", ntohs(server.sin_port));
/*
开始接收连接 */
listen(sock, 5);
len = sizeof(struct sockaddr);
do {
msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
if (msgsock == -1)
perror("accept");
else do{
memset(buf, 0, sizeof(buf));
if ((rval = recv(msgsock, buf, 1024)) < 0)
perror("reading stream message");
if (rval == 0)
printf("ending connection /n");
else
printf("-->%s/n", buf);
}while (rval != 0);
closesocket(msgsock);
} while (TRUE);
/*
因为这个程序已经有了一个无限循环,所以套接字 "sock" 从来不显式关闭。然而,当进程被杀死或正常终止时,所有套接字都将自动地被关闭。 */
exit(0);
}
客户方程序:
/* File Name: streamc.c */
#include
#include
#define DATA "half a league, half a league ..."
/*
这个程序建立套接字,然后与命令行给出的套接字连接;连接结束时,在连接上发送
一个消息,然后关闭套接字。命令行的格式是: streamc 主机名 端口号
端口号要与服务器程序的端口号相同 */
main(argc, argv)
int argc;
char *argv[ ];
{
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname( );
char buf[1024];
/*
建立套接字 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("opening stream socket");
exit(1);
}
/*
使用命令行中指定的名字连接套接字 */
server.sin_family = AF_INET;
hp = gethostbyname(argv[1]);
if (hp == 0) {
fprintf(stderr, "%s: unknown host /n", argv[1]);
exit(2);
}
memcpy((char*)&server.sin_addr, (char*)hp->h_addr, hp->h_length);
sever.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
perror("connecting stream socket");
exit(3);
}
if (send(sock, DATA, sizeof(DATA)) < 0)
perror("sending on stream socket");
closesocket(sock);
exit(0);
}

4.Windows socket 程序设计

Windows Sockets 是从 Berkeley Sockets 扩展而来的,其在继承 Berkeley Sockets 的基础上,又进行了新的扩充。这些扩充主要是提供了一些异步函数,并增加了符合 WINDOWS 消息驱动特性的网络事件异步选择机制。

Windows Sockets 由两部分组成:开发组件和运行组件。
开发组件: Windows Sockets 实现文档、应用程序接口 (API) 引入库和一些头文件。
运行组件: Windows Sockets 应用程序接口的动态链接库 (WINSOCK.DLL)

Microsoft Windows 下开发 Windows Sockets 网络程序与在 UNIX 环境下开发 Berkeley Sockets 网络程序有一定的差别,这主要时因为 Windows 是非抢先多任务环境,各任务之间的切换是通过消息驱动的。因此,在 Windows 下开发 Sockets 网络程序要尽量避开阻塞工作方式,而使用 Windows Sockets 提供的基于消息机制的网络事件异步存取接口。

Windows Sockets 为了支持 Windows 消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。基于这一策略, Windows Sockets 在如下方面作了扩充:

* 异步选择机制

UNIX Sockets 对于异步事件的选择是靠调用 select() 函数来查询的,这种方式对于 Windows 应用程序来说是难以接受的。 Windows Sockets 的异步选择函数提供了消息机制的网络事件选择,当使用它登记的网络事件发生时, Windows 应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。

* 异步请求函数

在标准 Berkeley Sockets 中,请求服务是阻塞的。 Windows Sockets 除了支持这一类函数外,还增加了相应的异步请求服务函数 WSAASyncGetXByY() 。这些异步请求函数允许应用程序采用异步方式获取请求信息,并且在请求的服务完成时给应用程序相应的窗口函数发送一个消息。

* 阻塞处理方法

Windows Sockets 为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃 CPU 让其它应用程序运行,它在调用处于阻塞时便进入一个叫“ HOOK ”的例程,此例程负责接收和分配 Windows 消息,这使得其它应用程序仍然能够接收到自己的消息并取得控制权。 Windows Sockets 还提供了两个函数 (WSASetBlockingHook() WSAUnhookBlockingHook()) 让用户设置和取消自己的阻塞处理例程,以支持要求复杂消息处理的应用程序(如多文档界面)。

* 出错处理

Windows Sockets 为了和以后多线程环境(如 Windows/NT )兼容,它提供了两个出错处理函数 WSAGetLastError() WSASetLastError() 来获取和设置当前线程的最近错误号,而不使用 Berkeley Sockets 中的全局变量 errno h_errno

* 启动与终止

对于所有在 Windows Sockets 上开发的应用程序,在它使用任何 Windows Sockets API 调用之前,必须先调用启动函数 WSAStartup() ,它完成 Windows Sockets DLL 的初始化;协商版本支持,分配必要的资源。在应用程序完成了对 Windows Sockets 的使用之后,它必须调用函数 WSACleanup() 来从 Windows Sockets 实现中注销自己,并允许实现释放为其分配的任何资源。

* 服务器端操作 socket (套接字)

在初始化阶段调用 WSAStartup()
此函数在应用程序中初始化 Windows Sockets DLL ,只有此函数调用成功后,应用程序才可以再调用其他 Windows Sockets DLL 中的 API 函数。在程式中调用该函数的形式如下: WSAStartup((WORD)((1<<8|1) ,( LPWSADATA &WSAData) ,其中 (1<<8|1) 表示我们用的是 WinSocket1.1 版本, WSAata 用来存储系统传回的关于 WinSocket 的资料。

建立 Socket
  初始化 WinSock 的动态连接库后,需要在服务器端建立一个 监听的 Socket ,为此可以调用 Socket() 函数用来建立这个监听的 Socket ,并定义此 Socket 所使用的通信协议。此函数调用成功返回 Socket 对象,失败则返回 INVALID_SOCKET( 调用 WSAGetLastError() 可得知原因,所有 WinSocket 的函数都可以使用这个函数来获取失败的原因 )

SOCKET PASCAL FAR socket( int af, int type, int protocol )
参数 : af: 目前只提供 PF_INET(AF_INET)
type
Socket 的类型 (SOCK_STREAM SOCK_DGRAM)
protocol
:通讯协定 ( 如果使用者不指定则设为 0)

如果要建立的是遵从 TCP/IP 协议的 socket ,第二个参数 type 应为 SOCK_STREAM ,如为 UDP (数据报)的 socket ,应为 SOCK_DGRAM

绑定端口

  接下来要为服务器端定义的这个监听的 Socket 指定一个地址及端口( Port ),这样客户端才知道待会要连接哪一个地址的哪个端口,为此我们要调用 bind() 函数,该函数调用成功返回 0 ,否则返回 SOCKET_ERROR
int PASCAL FAR bind( SOCKET s, const struct sockaddr FAR *name,int namelen );

参 数: s Socket 对象名;
name
Socket 的地址值,这个地址必须是执行这个程式所在机器的 IP 地址;
namelen
name 的长度;

如果使用者不在意地址或端口的值,那么可以设定地址为 INADDR_ANY ,及 Port 0 Windows Sockets 会自动将其设定适当之地址及 Port (1024 5000 之间的值 ) 。此后可以调用 getsockname() 函数来获知其被设定的值。

监听

当服务器端的 Socket 对象绑定完成之后 , 服务器端必须建立一个监听的队列来接收客户端的连接请求。 listen() 函数使服务器端的 Socket 进入监听状态,并设定可以建立的最大连接数 ( 目前最大值限制为 5, 最小值为 1) 。该函数调用成功返回 0 ,否则返回 SOCKET_ERROR

int PASCAL FAR listen( SOCKET s, int backlog );
参 数: s :需要建立监听的 Socket
backlog
:最大连接个数;

异步选择
服务器端的 Socket 调用完 listen ()后,如果此时客户端调用 connect ()函数提出连接申请的话, Server 端必须再调用 accept() 函数,这样服务器端和客户端才算正式完成通信程序的连接动作。为了知道什么时候客户端提出连接要求,从而服务器端的 Socket 在恰当的时候调用 accept() 函数完成连接的建立,我们就要使用 WSAAsyncSelect ()函数,让系统主动来通知我们有客户端提出连接请求了。该函数调用成功 返回 0 ,否则返回 SOCKET_ERROR

int PASCAL FAR WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent );
参数: s Socket 对象;
hWnd
:接收消息的窗口句柄;
wMsg
:传给窗口的消息;
lEvent
: 被注册的网络事件,也即是应用程序向窗口发送消息的网路事件,该值为下列值 FD_READ FD_WRITE FD_OOB FD_ACCEPT FD_CONNECT FD_CLOSE 的组合,各个值的具体含意为 FD_READ :希望在套接字 S 收到数据时收到消息; FD_WRITE :希望在套接字 S 上可以发送数据时收到消息; FD_ACCEPT :希望在套接字 S 上收到连接请求时收到消息; FD_CONNECT :希望在套接字 S 上连接成功时收到消 息; FD_CLOSE :希望在套接字 S 上连接关闭时收到消息; FD_OOB :希望在套接字 S 上收到带外数据时收到消息。
  具体应用时, wMsg 应是在应用程序中定义的消息名称,而消息结构中的 lParam 则为以上各种网络事件名称。所以,可以在窗口处理自定义消息函数中使用以下结构来响应 Socket 的不同事件:  

switch(lParam)
{

case FD_READ:

break;
case FD_WRITE


break;

}

服务器端接受客户端的连接请求
Client 提出连接请求时, Server hwnd 视窗会收到 Winsock Stack 送来我们自定义的一 个消息,这时,我们可以分析 lParam ,然后调用相关的函数来处理此事件。为了使服务器端接受客户端的连接请求,就要使用 accept() 函数,该函数新建一 Socket 与客户端的 Socket 相通,原先监听之 Socket 继续进入监听状态,等待他人的连接要求。该函数调用成功返回一个新产生的 Socket 对象,否则返回 INVALID_SOCKET

SOCKET PASCAL FAR accept( SCOKET s, struct sockaddr FAR *addr,int FAR *addrlen );
参数: s Socket 的识别码;
addr
:存放来连接的客户端的地址;
addrlen
addr 的长度

结束 socket 连接
结束服务器和客户端的通信连接是很简单的,这一过程可以由服务器或客户机的任一端启动,只要调用 closesocket() 就可以了,而要关闭 Server 端监听状态的 socket ,同样也是利用此函数。另外,与程序启动时调用 WSAStartup() 憨数相对应,程式结束前,需要调用 WSACleanup() 来通知 Winsock Stack 释放 Socket 所占用的资源。这两个函数都是调用成功返回 0 ,否则返回 SOCKET_ERROR

int PASCAL FAR closesocket( SOCKET s );
参 数: s Socket 的识别码;
int PASCAL FAR WSACleanup( void );
参 数: 无

* 客户端 Socket 的操作

建立客户端的 Socket
客户端应用程序首先也是调用 WSAStartup() 函数来与 Winsock 的动态连接库建立关系,然后同样调用 socket() 来建立一个 TCP UDP socket (相同协定的 sockets 才能相通, TCP TCP UDP UDP )。与服务器端的 socket 不同的是,客户端的 socket 可以调用 bind() 函数,由自己来指定 IP 地址及 port 号码;但是也可以不调用 bind() ,而由 Winsock 来自动设定 IP 地址及 port 号码。

提出连接申请
  客户端的 Socket 使用 connect() 函数来提出与服务器端的 Socket 建立连接的申请,函数调用成功返回 0 ,否则返回 SOCKET_ERROR

int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR *name, int namelen );
参 数: s Socket 的识别码;
name
Socket 想要连接的对方地址;
namelen
name 的长度

* 数据的传送
虽然基于 TCP/IP 连接协议(流套接字)的服务是设计客户机 / 服务器应用程序时的主流标准,但有些服务也是可以通过无连接协议(数据报套接字)提供的。先介绍一下 TCP socket UDP socket 在传送数据时的特性: Stream (TCP) Socket 提供双向、可靠、有次序、不重复的资料传送。 Datagram (UDP) Socket 虽然提供双向的通信,但没有可靠、有次序、不重复的保证,所以 UDP 传送数据可能会收到无次序、重复的资料,甚至资料在传输过程中出现遗漏。由于 UDP Socket 在传送资料时,并不保证资料能完整地送达对方,所以绝大多数应用程序都是采用 TCP 处理 Socket ,以保证资料的正确性。一般情况下 TCP Socket 的数据发送和接收是调用 send() recv() 这两个函数来达成,而 UDP Socket 则是用 sendto() recvfrom() 这两个函数,这两个函数调用成功发挥发送或接收的资料的长度,否则返回 SOCKET_ERROR

int PASCAL FAR send( SOCKET s, const char FAR *buf,int len, int flags );
参数: s Socket 的识别码
buf
:存放要传送的资料的暂存区
len buf
:的长度
flags
:此函数被调用的方式
对于 Datagram Socket 而言,若是 datagram 的大小超过限制,则将不会送出任何资料,并会传回错误值。对 Stream Socket 言, Blocking 模式下,若是传送系统内的储存空间不够存放这些要传送的资料, send() 将会被 block 住,直到资料送完为止;如果该 Socket 被设定为 Non-Blocking 模式,那么将视目前的 output buffer 空间有多少,就送出多少资料,并不会被 block 住。 flags 的值可设为 0 MSG_DONTROUTE MSG_OOB 的组合。

int PASCAL FAR recv( SOCKET s, char FAR *buf, int len, int flags );
参数: s Socket 的识别码
buf
:存放接收到的资料的暂存区
len buf
:的长度
flags
:此函数被调用的方式
  对 Stream Socket 言,我们可以接收到目前 input buffer 内有效的资料,但其数量不超过 len 的大小。

* 自定义的 CMySocket 类的实现代码:
根据上面的知识,自定义了一个简单的 CMySocket 类,下面是定义的该类的部分实现代码:

CMySocket::CMySocket() : file:// 类的构造函数
{
WSADATA wsaD;
memset( m_LastError, 0, ERR_MAXLENGTH );
// m_LastError 是类内字符串变量 , 初始化用来存放最后错误说明的字符串;
// 初始化类内 sockaddr_in 结构变量,前者存放客户端地址,后者对应于服务器端地址 ;
memset( &m_sockaddr, 0, sizeof( m_sockaddr ) );
memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
int result = WSAStartup((WORD)((1<<8|1) &wsaD);// 初始化 WinSocket 动态连接库 ;
if( result != 0 ) // 初始化失败;
{ set_LastError( "WSAStartup failed!", WSAGetLastError() );
return;
}
}

//////////////////////////////
CMySocket::~CMySocket() { WSACleanup(); }//
类的析构函数;
////////////////////////////////////////////////////
int CMySocket::Create( void )
{// m_hSocket 是类内 Socket 对象,创建一个基于 TCP/IP Socket 变量,并将值赋给该变量;
if ( (m_hSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP )) == INVALID_SOCKET )

{
set_LastError( "socket() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
///////////////////////////////////////////////
int CMySocket::Close( void )//
关闭 Socket 对象;
{
if ( closesocket( m_hSocket ) == SOCKET_ERROR )
{
set_LastError( "closesocket() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
file:// 重置 sockaddr_in 结构变量;
memset( &m_sockaddr, 0, sizeof( sockaddr_in ) );
memset( &m_rsockaddr, 0, sizeof( sockaddr_in ) );
return ERR_SUCCESS;
}
/////////////////////////////////////////
int CMySocket::Connect( char* strRemote, unsigned int iPort )//
定义连接函数;
{
if( strlen( strRemote ) == 0 || iPort == 0 )
return ERR_BADPARAM;
hostent *hostEnt = NULL;
long lIPAddress = 0;
hostEnt = gethostbyname( strRemote );// 根据计算机名得到该计算机的相关内容;
if( hostEnt != NULL )
{
lIPAddress = ((in_addr*)hostEnt->h_addr)->s_addr;
m_sockaddr.sin_addr.s_addr = lIPAddress;
}
else
{
m_sockaddr.sin_addr.s_addr = inet_addr( strRemote );
}
m_sockaddr.sin_family = AF_INET;
m_sockaddr.sin_port = htons( iPort );
if( connect( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
{
set_LastError( "connect() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;

}
///////////////////////////////////////////////////////
int CMySocket::Bind( char* strIP, unsigned int iPort )//
绑定函数;
{
if( strlen( strIP ) == 0 || iPort == 0 )
return ERR_BADPARAM;
memset( &m_sockaddr,0, sizeof( m_sockaddr ) );
m_sockaddr.sin_family = AF_INET;
m_sockaddr.sin_addr.s_addr = inet_addr( strIP );
m_sockaddr.sin_port = htons( iPort );
if ( bind( m_hSocket, (SOCKADDR*)&m_sockaddr, sizeof( m_sockaddr ) ) == SOCKET_ERROR )
{
set_LastError( "bind() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
//////////////////////////////////////////
int CMySocket::Accept( SOCKET s )//
建立连接函数, S 为监听 Socket 对象名;
{
int Len = sizeof( m_rsockaddr );
memset( &m_rsockaddr, 0, sizeof( m_rsockaddr ) );
if( ( m_hSocket = accept( s, (SOCKADDR*)&m_rsockaddr, &Len ) ) == INVALID_SOCKET )
{
set_LastError( "accept() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::asyncSelect( HWND hWnd, unsigned int wMsg, long lEvent )
file://
事件选择函数;
{
if( !IsWindow( hWnd ) || wMsg == 0 || lEvent == 0 )
return ERR_BADPARAM;
if( WSAAsyncSelect( m_hSocket, hWnd, wMsg, lEvent ) == SOCKET_ERROR )
{
set_LastError( "WSAAsyncSelect() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Listen( int iQueuedConnections )//
监听函数;

{
if( iQueuedConnections == 0 )
return ERR_BADPARAM;
if( listen( m_hSocket, iQueuedConnections ) == SOCKET_ERROR )
{
set_LastError( "listen() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
////////////////////////////////////////////////////
int CMySocket::Send( char* strData, int iLen )//
数据发送函数;
{
if( strData == NULL || iLen == 0 )
return ERR_BADPARAM;
if( send( m_hSocket, strData, iLen, 0 ) == SOCKET_ERROR )
{
set_LastError( "send() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ERR_SUCCESS;
}
/////////////////////////////////////////////////////
int CMySocket::Receive( char* strData, int iLen )//
数据接收函数;
{
if( strData == NULL )
return ERR_BADPARAM;
int len = 0;
int ret = 0;
ret = recv( m_hSocket, strData, iLen, 0 );
if ( ret == SOCKET_ERROR )
{
set_LastError( "recv() failed", WSAGetLastError() );
return ERR_WSAERROR;
}
return ret;
}
void CMySocket::set_LastError( char* newError, int errNum )
file://WinSock API
操作错误字符串设置函数;
{
memset( m_LastError, 0, ERR_MAXLENGTH );
memcpy( m_LastError, newError, strlen( newError ) );
m_LastError[strlen(newError)+1] = '/0';
}
有了上述类的定义,就可以在网络程序的服务器和客户端分别定义 CMySocket 对象,建立连接,传送数据了。例如,为了在服务器和客户端发送数据,需要在服务器端定义两个 CMySocket 对象 ServerSocket1 ServerSocket2 ,分别用于监听和连接,客户端定义一个 CMySocket 对象 ClientSocket ,用于发送或接收数据,如果建立的连接数大于一,可以在服务器端再定义 CMySocket 对象,但要注意 连接数不要大于五。

VC 中进行 WINSOCK API 编程开发的时候,需要在项目中使用下面三个文件,否则会出现编译错误。
1
WINSOCK.H: 这是 WINSOCK API 的头文件,需要包含在项目中。
2
WSOCK32.LIB: WINSOCK API 连接库文件。在使用中,一定要把它作为项目的非缺省的连接库包含到项目文件中去。
3
WINSOCK.DLL: WINSOCK 的动态连接库,位于 WINDOWS 的安装目录下。

socket通信


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论