Windows Socket API 使用经验
本文是我在进行MS-Windows、HP-unix网络编程的实践过程中总结出来的一些经验,仅供大家参考。本文所谈到的Socket函数如果没有特别说明,都是指的Windows Socket API。
一、WSAStartup函数
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
使 用Socket的程序在使用Socket之前必须调用WSAStartup函数。该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指 明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据 请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其 它Socket函数了。该函数执行成功后返回0。
例:假如一个程序要使用2.1版本的Socket,那么程序代码如下
wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
二、WSACleanup函数
int WSACleanup (void);
应用程序在完成对请求的Socket库的使用后,要调用WSACleanup函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源。
三、socket函数
SOCKET socket(
int af,
int type,
int protocol
);
应用程序调用socket函数来创建一个能够进行网络通 信的套接字。第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置PF_INET;第二个参数指定要创建的套接字类型,流套 接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM;第三个参数指定应用程序所使用的通信协议。
该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里 都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据 结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系 统的内核缓冲里。下面是一个创建流套接字的例子:
struct protoent *ppe;
ppe=getprotobyname("tcp");
SOCKET ListenSocket=socket(PF_INET,SOCK_STREAM,ppe->p_proto);
四、closesocket函数
int closesocket(
SOCKET s
);
closesocket 函数用来关闭一个描述符为s套接字。由于每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构, 因此有可能有几个套接字描述符指向同一个套接字数据结构。套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。当 调用closesocket函数时,操作系统先检查套接字数据结构中的该字段的值,如果为1,就表明只有一个套接字描述符指向它,因此操作系统就先把s在 套接字描述符表中对应的那条表项清除,并且释放s对应的套接字数据结构;如果该字段大于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把 s对应的套接字数据结构的引用次数减1。
closesocket函数如果执行成功就返回0,否则返回SOCKET_ERROR。
五、send函数
int send(
SOCKET s,
const char FAR *buf,
int len,
int flags
);
不 论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函 数来向客户程序发送应答。该函数的第一个参数指定发送端套接字描述符;第二个参数指明一个存放应用程序要发送数据的缓冲区;第三个参数指明实际要发送的数 据的字节数;第四个参数一般置0。这里只描述同步Socket的send函数的执行流程。当调用该函数时,send先比较待发送数据的长度len和套接字 s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;如果len小于或者等于s的发送缓冲区的长度,那么 send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没 有数据,那么send就比较s的发送缓冲区的剩余空间和len,如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果 len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传 的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。如果send函数copy数据成功,就返回实际copy的字节数,如果 send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR)
注意:在unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
六、recv函数
int recv(
SOCKET s,
char FAR *buf,
int len,
int flags
);
不 论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。该函数的第一个参数指定接收端套接字描述符;第二个参数指明一个缓冲区,该缓 冲区用来存放recv函数接收到的数据;第三个参数指明buf的长度;第四个参数一般置0。这里只描述同步Socket的recv函数的执行流程。当应用 程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错 误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如 果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲 中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy 完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它 返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
注意:在unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
七、bind函数
int bind(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);
当 创建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。一个服务程序必须调用bind函数来给其绑定一个IP地址和一个特定 的端口号。客户程序一般不必调用bind函数来为其Socket绑定IP地址和断口号。该函数的第一个参数指定待绑定的Socket描述符;第二个参数指 定一个sockaddr结构,该结构是这样定义的:
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
sa_family指定地址族,对于TCP/IP协议族的套接字,给其置AF_INET。当对TCP/IP协议族的套接字进行绑定时,我们通常使用另一个地址结构:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
其 中sin_family置AF_INET;sin_port指明端口号;sin_addr结构体中只有一个唯一的字段s_addr,表示IP地址,该字段 是一个整数,一般用函数inet_addr()把字符串形式的IP地址转换成unsigned long型的整数值后再置给s_addr。有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其Socket绑定IP地址时 可以把htonl(INADDR_ANY)置给s_addr,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的 服务程序的Socket绑定一个固定的IP地址,那么就只有与该IP地址处于同一个网段上的客户程序才能与该服务程序通信。我们用0来填充 sin_zero数组,目的是让sockaddr_in结构的大小与sockaddr结构的大小一致。下面是一个bind函数调用的例子:
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));
八、listen函数
int listen( SOCKET s, int backlog );
服务程序可以调用listen函数使其流套接字s处于监听状态。处于监听状态的流套接字s将维护一个客户连接请求队列,该队列最多容纳backlog个客户连接请求。假如该函数执行成功,则返回0;如果执行失败,则返回SOCKET_ERROR。
九、accept函数
SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);
服 务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接 通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回INVALID_SOCKET。该函数 的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结 构的长度。下面是一个调用accept的例子:
struct sockaddr_in ServerSocketAddr;
int addrlen;
addrlen=sizeof(ServerSocketAddr);
ServerSocket=accept(ListenSocket,(struct sockaddr *)&ServerSocketAddr,&addrlen);
十、connect函数
int connect(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);
客户程序调用connect函数来使客户Socket s与监听于name所指定的计算机的特定端口上的服务Socket进行连接。如果连接成功,connect返回0;如果失败则返回SOCKET_ERROR。下面是一个例子:
struct sockaddr_in daddr;
memset((void *)&daddr,0,sizeof(daddr));
daddr.sin_family=AF_INET;
daddr.sin_port=htons(8888);
daddr.sin_addr.s_addr=inet_addr("133.197.22.4");
connect(ClientSocket,(struct sockaddr *)&daddr,sizeof(daddr));
转自:http://wangxiaojs.iteye.com/blog/294578
如何进行Socket编程(转)
进程通信的概念最初来源于单机系统,由于每个进程都在各自的地址范围内运行,为了保证两个相互通信的进程之间既不互相干扰,又协调一致的工作,操作系统为进程通信提供了相应设施,如UNIX BSD中的管道(pipe),有名管道(named pipe)和软中断信号(singal),UNIX system V的消息(message)、共享存储区(shared memory)和信号量(semaphore)等,但都局限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的通信问题(可把同机进程通信看作其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可以用进程号(pid)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如主机A赋予某进程号5,在B主机也可以存在5号进程,因此5号进程这句话就没有意义了。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。为了解决上述问题,TCP/IP协议引入了下列几个概念。
端口
网络中可以被命名和寻址的通信端口是操作系统可分配的一种资源。按照OSI七层协议的描述,传输层与网络层最大的区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅是主机地址了,还包括可以描述进程的某种标识符。为此TCP/IP协议提出了协议端口的概念,用于标识通信的进程。
端口是一种抽象的软件结构,包括一些数据结构和I/O缓冲区。应用程序即进程通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都从该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问。
类似于文件描述符,每个端口都拥有一个叫端口号的整数描述符,以区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是两个完全独立的软件模块,因此各自的端口号也相互独立。如TCP有一个255号端口,UDP也可以有一个255号端口,两者并不冲突。
端口号的分配是一个重要问题,有两种基本分配方式:第一种叫全局分配这是一种集中分配方式,由一个公认的中央机构根据用户需要尽行统一分配,并将结果公布于众,第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回本地唯一的端口号,进程再通过合适的系统调用,将自己和该端口连接起来(绑定)。TCP/IP端口号的分配综合了两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口叫周知口,即使在不同的机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP规定,小于256的端口才能作为保留端口。
地址
网络通信中的两个进程分别在两个不同的机器上。在互连网络中,两台机器可以位于不同的网络,这些网络通过网际互连设备(网关,网桥,路由器)连接。因此需要三级寻址。
1。某一主机与多个网络相连,必须指定一特定网络地址;
2。网络上美一台主机应有其唯一的地址;
3。美一主机上的每一进程应有在该主机上的唯一标识。
主机地址就是IP啦,不必多说。进程唯一标识符是十六位整数端口号。
网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节,有的则相反。为保证数据的正确性,在网络协议中需指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,他们均含在协议的头文件中。
连接
两个进程间的通信链路称为连接。连接在内部表现为一些缓冲区和一组协议机制,在外部表现出比无连接高的可靠性。
半相关
综上所述,网络中用一个三元组可以在全局中唯一标是一个进程:(协议,本机地址,本地端口号)这样一个三元组,叫做一个半相关,他指定连接的每半部分。全相关
一个完整的网间进程通信需要有两个进程组成,并且只能使用同一种高层协议。也就是说TCP和UDP没法通信。因此一个完整的网间进程通信需要一个五元组来标识:
(协议,本机地址,本地端口号,远地地址,远地端口号)这样一个五元组叫做一个全相关。
在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户机/服务器模式,即客户端向服务器发出请求,服务器接收到请求后提供相应的服务客户机/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软、硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立一种联系,为二者的数据交换提供同步,这就是基于客户机/服务器模式的TCP/IP。
客户机/服务器模式在操作过程中采取的是主动请求方式:
首先服务器方要启动,并根据请求提供相应服务:
1。打开一通信通道并告知本地主机,它愿意在某一公认地址端口上(周知口,如http为80)接受客户请求。
2。等待客户请求到达该端口。
3。接收到重复服务请求,处理该请求并发送应答信号。接收并发服务请求,要激活一新进程来处理这个客户请求。新进程处理此客户请求,并不需要对其他请求做出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
4。返回第二步,等待另外的客户请求
5。关闭服务器。
客户方:
1。打开一通信通道,并连接到服务器所在主机的特定端口。
2。向服务器发出服务请求报文,等待并接收应答;继续提出请求。
3。请求结束后关闭通信通道并终止。
从上面的描述过程可知:
1。客户与服务器进程的作用是非对称的。因此编码不同。
2。服务进程一般是先于客户请求启动的。只要系统运行,该进程一直存在,直到正常终止或者强迫终止。
在UNIX世界中,网络应用编程界面有两类:BSD的套接字SOCKET和SYSTEM V的TLI.由于Sun公司采用了支持TCP/IP的BSD系统,TCP/IP的应用有了更大发展其网络应用编程界面Socket在网络编程中已成为标准。并且也早已经进入了MS的世界。
TCP/IP的Socket提供下列三种类型的套接字
1。流式套接字(SOCKET_STREAM)
提供了一个面向连接,可靠的数据传输服务,数据无差错,无重复的发送且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。FTP协议即采用流式套接字。
2。数据报式套接字(SOCKET_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序无序。网络文件系统NFS使用数据报式套接字。
3。原始式套接字(SOCKET_RAW)
该接口允许对较低层次协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
基本套接字调用
创建套接字--socket();
绑定本机端口--bind();
建立连接--connect(),accept();
侦听端口--listen();
数据传输--send(),recv();
输入/输出多路复用--select();
关闭套接字--closesocket();
不论何种语言,和socket打交道都是这一组调用只是在格式上有一点点差别。我只使用过c和perl,再加上这里又不让出现与perl无关的东西,那下面就主要讨论PERL的socket编程啦:
创建建套接字:
socket(SOC_VARIABLE,DOMAIN_FLAG,connectType,num) #相应的C语言调用为sockid=socket(af,type,protocol)
参数含义如下:
SOC_VARIABLE是用于建立套接的句柄,相当于c里面的sockid号;DOMAIN_FLAG叫域标记,在C里相当于af--address family,地址族。地址族和域是一个概念,其实就是平常所说的域。UNIX支持的域类型有
AF-UNIX; UNIX内部地址
AF-INET; TCP/IP地址
AF-NS; Xerox NS地址
AF-APPLETALK; Apple的Appletalk地址
而dos/windows支持的域地址族只有AF-INET.所以大部分的socket编程都只用到它。
connectType(c里的type)就是前面所说的三种socket类型。num相当于c里面的protocol那大家一看就明白了这是协议号,用来指定SOCKET请求所希望的协议,这个参数不一定起作用,当前两个参数可以确定协议时可以取值为零。
所以,一个完整的PERL的建立socket如下
socket(THESCK,AF-INET,SOCKET_STREAM,getprotocolbyname('tcp'));
#c语言: int sockid;
# sockid = socket(AF-INET,SOCKET_STREAM,0);
第二步了:bind()--绑定到本地地址。
第一步socket()调用只是指定了相关五元组的协议元。其他四元需要别的调用来补充。socket的创建可以认为创建了一个名字空间(地址族),但他没有被命名。bind()将套接字地址与本机创建的套接字句柄(c里面的套接字号)绑定,即将名字赋予套接字(句柄),以指定本地半相关。按照标准的socket来说(在UNIX世界里所谓编程的"标准接口"就和说"c的编程接口"没啥区别),套接字地址是一个描述socket地址的数据结构。其中描述TCP/IP协议地址(AF_INET)的结构为:
struct sockaddr_in{
short sin_family; //AF_INET
u_short sin_port; //16位端口号,网络字节顺序
struct in_addr sin_addr; //32位IP地址,网络字节顺序
char sin_zero[8]; //保留
}
其他的结构还有 sockaddr_ns,sockaddr_un等,是用在其他协议地址的。基本上我们用不到。于是一个标准绑定为:
bind(SOCKET sockid,struct * localaddr_name,int addrlen);
//sockid是一个未命名套接字的socket号
//localaddr_name是用来命名sockid的指向sockaddr_in结构的指针
//addrlen是localaddr_name的字节长度
使用perl的bind()时先要调用inet_aton('localhost');或函数 INADDR_ANY 取得ip地址字符串,然后调用
$localaddr_port = sockaddr_in($port,inet_aton('localhost')); #$port是端口号
或者是 $localaddr_port = sockaddr_in($port,INADDR_ANY); 获得TCP/IP的地址,最后bind(SERVER,$localaddr_port);
完成绑定!在这里不用指定$localaddr_port的字节长度,这就是perl的好处。
这两个系统调用用于完成一个全相关的建立,其中connect()用于建立连接。accept()使服务器等待来自某客户进程的实际连接。其调用格式如下
connect(SOCKET sockid,struct sockaddr* destaddr,int addrlen);
//sockid是欲建立连接的本地socket号
//destaddr是一个指向对方socket地址(信宿地址)结构的指针
//addrlen是对方socket地址长度
perl语言的connect()调用格式为:
connect(SOC_VARIABLE,NAME_VARIABLE)
具体的调用过程如下
$remoteaddr_port = sockaddr_in($port,inet_aton('abc.efg.com'));
connect(CLIENT,$remoteaddr_port); #半相关三元组(协议,远地地址,远地端口号)。
可以发现connect()和bind()调用如出一辙,只是SERVER换成CLIENT,local换成remote,没错,他们的道理是一样的,作用是互补的。他们各自建立了服务器和客户机方面的半相关。这时就要accept()来掺和一下一个完整的网间进程通信的全相关就可以建立啦!(其实标准connect()还可以用于无连接的socket调用。但这个用法比较左道,常把人搞晕点,所以就不说了)
标准accept()调用:
SOCKET newsock = accept(SOCKET sockid,struct sockaddr* clientaddr,int addlen)
//sockid ,服务器本地的socket号
//clientaddr ,指向客户socket结构的指针。它的初始值为空
//addlen ,客户socket结构的字节长度,它的初始值为0
//newsock ,accept()的返回值,为一个新的socket号,可用于服务器 //处理并发请求。服务器fork一个子服务器进程,利用此socket号回
//答accept()所接收的客户请求
可以看出,accept()是面向连接的服务器端的调用。他还将客户端的socket地址及其字节长度放在clientaddr和addlen以便为其他无连接的调用指定信宿地址。但是,那些比较灵活但没有原则的无连接用法在perl里是没有的,perl的用法认为socket必须是面向连接的,请看perl里的accept():
accept(NEW_SOC_VARIABLE,CURRENT_SOC_VARIABLE);
可以看到accept()从当前的socket句柄上连接出一个客户端到新的socket句柄。而它的返回值客户端的地址(信宿地址)。其实连接一旦建立,服务方无须知道信宿地址,只要向套口里灌输比特流就行了。这样做的好处是使协议对于应用程序更加透明,易于理解。
在调用accept()之前还应该先调用listen(),listen()的作用是侦听端口,接收连接。如果不调用listen()的话,accept()将无法从当前socket"接"回与客户端的连接。标准listen():
listen(sockid,quelen);
//socket号,服务器愿意从它上面接收请求
//quelen,请求队列长度,listen()以此限制排队请求的个数
perl的liste():
listen(SOC_VARIABLE,num);#和C语言的版本很像哦
SOC_VARIABLE是socket句柄,num是请求队列长度.
到这里为止,一个连接的五元组都齐了。
http://gnn.iteye.com/blog/464862