前置知识
1.有一定C++编程基础
2.套接字的主要类型
有两种:一种是流式套接字(SOCK_STREAM),一种是数据报套接字(SOCK_DGRAM),
分别用于TCP/IP协议和UDP协议进行通讯的应用程序中。
3.TCP/IP寻址方式
用户使用IP地址和端口号进行确定通信双方。
-----------------------------------------------------------------------------
一、Winsock编程流程
1.配置编程环境
由于所有的Winsock函数均是从动态链接库WS2_32.DLL中导出的,而VC默认是没有与该库进行连接,所以我们需要进行编程环境设置。添加方法是选择“工程--->设置--->连接”
2.初始化套接字库
调用WSAStartup()对该库进行初始化,查询MSDN后得知原型如下
int WSAStartup(WORD wVersionRequested , LPWSADATA lpWSAData);
该函数调用成功,将返回0。
参数wVersionRequested表示当前套接字库的版本号。
例如当前套接字版本号为2.0,则将该参数设置为2.0,代码如下:
<pre class="cpp" name="codeWORD wVersionRequested=MAKEWORD(2,0);参数lpWSAData指向结构体WSADATA的指针变量,表示获取到的套接字库详细信息。
typedef struct WSAData {
WORD wVersion; //库文件建议使用的版本号
WORD wHighVersion; //库文件支持的最高版本
char szDescription[WSADESCRIPTION_LEN+1]; //描述库文件的字符串
char szSystemStatus[WSASYS_STATUS_LEN+1]; //系统状态字符串
unsigned short iMaxSockets; //用于设置客户端的最大连接数量
unsigned short iMaxUdpDg; //新版本已经无用
char FAR * lpVendorInfo; //新版本已经无用
} WSADATA, *LPWSADATA;
例1:有了上述知识,就可以开始初始化套接字库了,代码如下:
WSADATA data; //定义WSAData指针对象
WORD w=MAKEWORD(2,0); //定义当前套接字库版本号
::WSAStartup(w,&data); //初始化套接字库,为默认参数
3.创建套接字句柄
创建套接字句柄的函数是socket(),查询MSDN后得知该函数原型如下
SOCKET socket(
int af, //指定套接字所使用的地址格式,TCP/IP设置为AF_INET
int type, //套接字类型,见前置知识第2条。
int protocol //一般设置为0,想深究,请百度。
);
该函数执行成功,将返回新创建的套接字句柄。否则,将返回INVALID_SOCKET。
例2:创建流式套接字(TCP/IP)句柄的代码如下:
SOCKET s; //定义套接字句柄对象
s=::socket(AF_INET,SOCK_STREAM,0); //创建并返回套接字句柄
4.构造套接字地址结构
sockaddr_in是TCP/IP地址家族中统一的套接字地址结构,查询MSDN得知原型如下
struct sockaddr_in{
short sin_family; //指定地址家族,TCP/IP设置为AF_INET
unsigned short sin_port; //端口
struct in_addr sin_addr; // IP地址
char sin_zero[8]; //一般设置为0
};
该结构成员变量in_addr结构定义如下:
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
通常,我们在编写网络程序时,使用一个u_long类型的字符串进行描述IP地址。
例3:127.0.0.1是回送地址,指向本机,一般可以用来测试,代码如下:
sockaddr_in addr; //定义结构对象
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
编程知识普及:字节顺序
字节顺序分为:Big-Endian和Little-Endian。以unsigned int val=0x12345678为例
Intel CPU(Little-Endian)存放的字节顺序为0x78、0x56、0x34、0x12。
而网络的字节顺序为(Big-Endian) 0x12、0x34、0x56、0x78。正好相反,但更符合人
们的阅读情况。所以编程人员将数据通过网络发送时,需要将存储在本地计算机上的数
据转换成以网络字节顺序排列的数据。从数据的角度来说,网络字节顺序将最重要的数
据先进行存储,而Intel CPU字节顺序则将不重要的字节首先储存。
字节顺序转换函数
u_short htons(u_short hostshort);
//将一个u_short类型的端口号从主机字节顺序转换到网络字节顺序。
u_short ntohs(u_short netshort);
//将一个u_short类型的端口号从网络字节顺序转换到主机字节顺序。
u_long htonl(u_long hostlong);
//将一个u_long类型的IP地址从主机字节顺序转换到网络字节顺序。
u_long ntohl(u_long netlong);
//将一个u_long类型的IP地址从网络字节顺序转换到主机字节顺序。
unsigned long inet_addr(const char FAR *cp);
//将一个字符串IP转换到以网络字节顺序排列的IP地址。
char FAR * inet_ntoa(struct in_addr in);
//将一个以网络字节顺序排列的IP地址转化为一个字符串IP。
例4:在构造套接字地址结构中使用字节顺序转换函数
sockaddr_in addr; //定义套接字地址结构对象
addr.sin_family=AF_INET; //指定地址家族为TCP/IP
addr.sin_port=htons(80); //指定端口号
addr.sin_addr.S_un.S_addr="127.0.0.1"; //将字符串IP转换为网络字节顺序排列的IP
char addRes[]=inet_ntoa(addr.sin_addr.S_un.S_addr); //将网络字节顺序排列的IP转换为字符串IP
5.绑定地址信息(服务端)
对于服务器,套接字创建成功后,还应该使用bind()函数将套接字与地址结构信息相
关联,bind()函数原型如下:
int bind(
SOCKET s, //套接字句柄
const struct sockaddr FAR *name, //地址结构信息
int namelen //地址结构大小
);
该函数调用成功,则返回0。
例5:将套接字句柄绑定到本地地址,代码如下:
sockaddr_in addr; //定义套接字地址结构对象
addr.sin_family=AF_INET; //指定地址家族为TCP/IP
addr.sin_port=htons(80); //指定端口号
addr.sin_addr.S_un.S_addr=INADDR_ANY; //服务器能接受任何计算机发来的请求
::bind(s,(sockaddr*)&addr,sizeof(addr));//绑定套接字到指定地址结构
6.监听端口信息(服务端)
服务器程序可以调用listen()函数实现监听端口的功能,函数原型如下:
int listen(
SOCKET s, //实现监听功能的套接字句柄
int backlog //指定监听的最大连接数量
);
如果多个客户端同时向服务器发出连接请求,并且超过了最大连接数,客户端将返回错误代码。
例6:在套接字上进行监听,并且将最大监听数量定义为5:
::listen(s,5);
7.连接(客户端)
客户端连接服务端需要用到connect()函数,函数原型如下:
int connect(
SOCKET s, //实现连接功能的套接字句柄
const struct sockaddr FAR *name, //将要连接的服务器地址信息的结构指针
int namelen //服务器地址信息结构体长度
);
例7:连接地址为"127.0.0.1",端口为80的服务器。代码如下:
sockaddr_in addr; //定义套接字地址结构对象
addr.sin_family=AF_INET; //指定服务器地址家族为TCP/IP
addr.sin_port=htons(80); //指定要连接的端口号
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//指定服务器IP地址
::connect(s,(sockaddr*)&addr,sizeof(addr)); //连接服务器
8.接受连接请求(服务端)
当服务端收到客户端的连接请求时,则可以调用accept()函数接受该请求,函数原型:
SOCKET accept(
SOCKET s, //套接字句柄
struct sockaddr FAR *addr, //保存客户端的地址信息于addr中
int FAR *addrlen //保存客户端的地址信息长度于addrlen中
);
例8:服务端套接字句柄为s,使用accept()函数接收客户端信息,代码如下:
SOCKET s1;
sockaddr_in addrConnect;
int n=sizeof(addrConnect);
s1=::accept(s,(sockaddr*)&addrConnect,&n);
9.数据收发
当服务器和客户端成功连接时,我们可以使用send()和recv()函数实现数据的发送和接收功能,函数原型如下:
int send(SOCKET s, const char FAR *buf, int len, int flags);
int recv(SOCKET s, char FAR *buf, int len, int flags);
两个函数的各个参数以及表示的意义都是相同的,参数*buf是指向数据缓冲区的指针变量,参数len是数据的长度,参数flags一般设置为0。
注意:对于服务端,参数s应该为接收客户端连接请求后,返回的新套接字句柄。
对于客户端,参数s应该为客户端创建的套接字句柄。
例9:使用send()、recv()函数来发送、接收数据,代码如下:
char szText[]="Hello C++ Socket World"; //用于储存发送的数据
::send(s1,szText,sizeof(szText),0); //发送数据给客户端s1
char szText2[100]={0}; //用于保存接收到的数据
::recv(s,szText2,sizeof(szText2),0); //接收服务端发来的数据
10.关闭套接字
当套接字使用完毕或者程序退出时,应该调用函数closesocket()关闭套接字句柄,函数原型如下:
int closesocket(
SOCKET s //需要关闭的套接字句柄
);
例10:关闭之前创建的套接字句柄s,代码如下:
::closesocket(s);
11.释放套接字库
例11:当程序退出时,还应该记得调用WSACleanup()函数释放该套接字库,代码如下:
::WSACleanup();
二、Winsock编写过程
服务端编写过程: 客户端编写过程:
1.初始化套接字库:WSAStartup() 1.初始化套接字库:WSAStartup()
2.创建套接字句柄:socket() 2.创建套接字句柄:socket()
3.构造套接字地址结构:sockaddr_in 3.构造套接字地址结构:sockaddr_in
4.绑定地址信息:bind() 4.连接服务端:connect
5.监听端口信息:listen() 5.数据收发:send()、recv()
6.接受连接请求:accept() 6.关闭套接字:closesocket()
7.数据收发:send()、recv() 7.释放套接字库:WSACleanup()
8.关闭套接字:closesocket()
9.释放套接字库:WSACleanup()