socket编程是所有协议实现的底层,任何协议都可以用socket来实现。
Winsock启动
winsock服务是以动态链接库Winsock DLL形式实现的,所以必须先对Winsock DLL进行初始化,协商Winsock的版本支持,并分配必要的资源,函数原型为:
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
参数wVersionRequested用于指定准备加载的Winsock库的版本,高位字节表示副版本,低位字节表示主版本。
lpWSAData是指向WSADATA类型的指针,该结构中包含了加载的库版本相关信息。WSADATA结构如下:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYSSTATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
}WSADATA,*LPWSADATA;
举个例子,想加载主版本副版本都为2的Winsock库:
WASDATA WSAData;
WSAStartup(0x0202,&WSAData);
WSAData中wVersion返回希望加载的Winsock版本。wHighVersion返回现有Winsock库的最高版本。
Winsock终止
程序结束时,应该终止对Winsock DLL的使用,并释放资源,以备下次使用。函数原型如下:
int WSACleanup(void);
成功调用则返回0,否则返回错误。
Winsock错误检查
一般情况下,Winsock API失败时返回SOCKET_ERROR,这个时候可以用WSAGetLastError函数来得到最近一次发生错误的错误码,函数原型如下:
int WSAGetLastError(void);
这些值都在Winsock1.h或者Winsock2.h中定义。
Winsock流套接字客户端编程模型
建立套接字
假设以面向连接的流套接字的客户端为例,首先建立一个windows 套接字socket,函数原型为:
SOCKET socket(int af,int type,int protocol);
其中,af用于指定网络地址类型,一般情况下取为AF_INET
type表示套接字类型,SOCK_STREAM表示创建流套接字,SOCK_DGRAM表示创建数据包套接字
protocol用于指定网络协议,默认为0,表示TCP/IP协议,其他例如IPPROTO_UDP表示UDP协议
所以创建一个流套接字如下所示:
SOCKET sock(AF_INET,SOCK_STREAM,0);
如果创建失败则返回INVALID_SOCKET错误。
客户端发出连接请求
创建流套接字成功之后,就需要和服务器建立连接,函数原型为:
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen);
s表示一个未连接的套接字,name是针对TCP的套接字地质结构,用于标识服务器进程的IP、PORT等信息,namelen一般取为sizeof(name),用于标识name的长度。
若name中地址域全为零的话,则会产生WSAEADDRNOTAVAIL错误。
若服务器没有侦听这一端口的进程,则会产生WSAECONNREFUSED错误。
若因网络原因连接超时,则会产生WSAETIMEOUT错误。
数据传输
一旦连接成功,就可以互相进行数据的发送和接收了,发送函数原型如下:
int send(SOCKET s,const char* buf,int len,int flags);
s表示已连接的套接字。
buf是发送字节缓冲区。
len表示要发送的字节个数。
flags取值包括0、MSG_DONTROUTE(禁止路由)、MSG_OOB(数据带外发送),一般情况下取0。
接收函数原型如下:
int recv(SOCKET s,char* buf,int len,int flags);
flags可以是0、MSG_PEEK(有用数据复制到提供的接收端缓冲区,但不从系统缓冲区删除)、MSG_OOB。
recv函数返回接收字节数。若失败,则返回SOCKET_ERROR。
关闭套接字
当所有任务完成之后,因必须关掉连接以释放套接字占用的所有资源。
为了避免数据丢失,先通过shutdown函数来中断连接,然后再调用closesocket关闭套接字。shutdown函数原型为:
int shutdown(SOCKET s,int how);
how用于描述终止那些操作,取值有SD_RECEIVE(表示不允许在调用接收函数),SD_SEND(表示不允许在调用发送函数),以及SD_BOTH。
closesocket用于释放套接字占用的所有资源。函数原型如下:
int closesocket(SOCKET s);
Winsock流套接字服务器端编程模型
建立套接字
假设以面向连接的流套接字的客户端为例,首先建立一个windows 套接字socket,函数原型为:
SOCKET socket(int af,int type,int protocol);
其中,af用于指定网络地址类型,一般情况下取为AF_INET,表示此socket作用于internet域。
type表示套接字类型,SOCK_STREAM表示创建流套接字,SOCK_DGRAM表示创建数据包套接字。
protocol用于指定网络协议,默认为0,表示TCP/IP协议,其他例如IPPROTO_UDP表示UDP协议。
所以创建一个流套接字如下所示:
SOCKET sock(AF_INET,SOCK_STREAM,0);
如果创建失败则返回INVALID_SOCKET错误。
绑定本地地址
将本地地址绑定到所创建的套接字上一边在网络上表示该套接字。bind函数原型如下:
int bind(SOCKET s,const struct sockaddr * name, int namelen);
name参数是该套接字绑定的本地地址。struct sockaddr结构如下:
struct sockaddr{
u_short sa_family;
char sa_data[14];
}
sa_family即是网络地址类型,必须设为AF_INET,表示此socket作用于internet域。
sa_data会因为所用协议不同而产生变化,故而在一般的TCP/IP协议中定义了一个与sockaddr大小一样的结构体sockaddr_in,在TCP/IP写一下,可以方便的互相转换。
struct sockadd_in{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char size_zero[8];
}
sin_port:指定服务器侦听端口,需要把端口号从主机顺序转换成网络顺序,htons()用于把端口号从主机顺序转换成网络顺序,ntohs反过来。
sin_addr:把一个主机ip地址保存成一个4字节的无符号长整形数。
inet_addr()用于把一个点式ip转换成一个32位无符号长整形,并且已经是网络顺序,不需要再调用htonl()将其转换成网络顺序。
inet_ntoa()反过来,讲一个struct in_addr结构的地址转换成一个char*字符串,此字符串存储在windows套接字专用的内存中,即每次调用inet_ntoa()返回的字符串都在同一地址。
有一个特殊的ip地址INADDR_ANY,使用时需要使用htonl()转换,他表示服务器可以监听本地所有地址,当主机有多个网卡时有用。
sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=htonl(INADDR_ANY);//addr.sin_addr.s_addr=inet_addr(“172.16.200.254”);
addr.sin_port = htons(port);
赋值的时候不是给结构体变量sin_addr赋值,而是向sin_addr.s_addr赋值,而inet_ntoa()的参数则是struct in_addr类型。
开始监听模式
将套接字置于监听模式,函数原型如下:
int listen(SOCKET s,int backlog);
backlog用于指定正在等待连接的最大队列长度,而不是已连接最大队列长度。
接收连接请求
使监听套接字做好接受客户端连接的准备,函数原型如下:
SOCKET accept(SOCKET s,struct sockaddr * addr,int * addrlen);
函数返回时,addr是发出连接请求的客户端的ip地址信息,并且返回一个新的套接字描述符,之后就应该用这个新套接字和客户端进行所有操作。
当无连接请求时,此函数被阻塞。