注:本系列文章适合初学网络编程的读者
网络程序的实现可以有很多方式,Windows Socket就是其中一种比较简单的方法。socket是连接应用程序与网络驱动程序的桥梁,socket在应用程序中创建,通过绑定操作与驱动程序建立关系。此后,应用程序送给socket的数据,由socket交给驱动程序向网络上发送出去。计算机从网络上收到与该socket绑定的IP地址和端口号相关的数据后,由驱动程序交给socket,应用程序便可从该socket中提取接收到的数据。
在TCP/IP网络应用中,通信的两个进程间相互作用的主要是(client/server)模式,即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。
下面通过一个简单的实例来讲述基于TCP的socket编程的通信流程。其中服务器端程序实现代码TCPSrv.cpp如下:
Server
1//#include <windows.h>
2#include <Winsock2.h>
3#include <stdio.h>
4
5void main()
6{
7 //加载套接字库
8 WORD wVersionRequested;
9 WSADATA wsaData;
10 int err;
11
12 wVersionRequested = MAKEWORD(1, 1);
13 err = WSAStartup(wVersionRequested, &wsaData);
14 if( err != 0 )
15 {
16 return;
17 }
18 if( LOBYTE(wsaData.wVersion) != 1 ||
19 HIBYTE(wsaData.wVersion) != 1)
20 {
21 WSACleanup();
22 return;
23 }
24
25 //创建用于监听的套接字
26 SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
27 SOCKADDR_IN addSrv;
28 addSrv.sin_addr.S_un.S_addr = htonl( INADDR_ANY );
29 addSrv.sin_family = AF_INET;
30 addSrv.sin_port = htons(6000);
31
32 //绑定套接字
33 bind(sockSrv, (SOCKADDR*)&addSrv, sizeof(SOCKADDR));
34 //将套接字设为监听模式,准备接收客户请求
35 listen(sockSrv, 5);
36
37 SOCKADDR_IN addClient;
38 int len = sizeof(SOCKADDR);
39
40 while(1)
41 {
42 //等待客户请求
43 SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addClient, &len);
44 char sendBuf[1000];
45 sprintf(sendBuf, "Welcome %s to http://www.cnblogs.com/lantionzy", inet_ntoa(addClient.sin_addr));
46 //发送数据
47 send(sockConn, sendBuf, strlen(sendBuf)+1, 0);
48
49 char receiveBuf[1000];
50 //接收数据
51 recv(sockConn, receiveBuf, 1000, 0);
52 //打印接收的数据
53 printf("%s\n", receiveBuf);
54 //关闭套接字
55 closesocket(sockConn);
56 }
57}
在这段代码中,首先定义了一个WORD类型的变量:wVersionRequested,用来保存WinSocl库的版本号,接着调用MAKEWORD宏创建一个包含了请求版本号的WORD值,之后调用WSAStartup函数加载套接字库,如果其返回值不等于0 ,则程序退出。接下来判断wsaData.wVersion的低字节和高字节是否都等于1,如果不是我们请求的版本,那么调用WSACleanup函数终止对Winsock库的使用并返回。
加载套接字库后,就可以按照一定流程来编写实现代码了:
1、创建套接字(socket)
利用socket函数创建套接字,对于它来说,第一个参数只能是AF_INET或(PF_INET);本例是基于TCP协议的网络程序,需要创建的是流式套接字,因此将socket函数第二个参数设置为SOCK_STREAM;将其第三个参数指定为0。这样该函数将根据地址格式和套接字类别,自动选择一个合适的协议。
2、将套接字绑定到一个本地地址和端口上(bind)
在SOCKADDR_IN结构体中,除了sa_family成员外,其他成员都是按网络字节顺序表示的。因此使用htonl函数将INADDR_ANY值转换为网络字节顺序。调用bind函把套接字sockSrv绑定到本地地址和指定端口上。其第一个参数为要绑定的套接字,第二个需要一个指针,可以用取地址符来实现,并且addrSrv变量是SOCKADDR_IN结构体类型,而这里需要的是SOCKADDR*类型,所以要进行强制转换。第三个参数是指定地址结构的大小,可以利用sizeof操作符来获取。
3、将套接字设为监听模式(listen),准备接收客户请求。其中listen函数第二个参数是指等待连接队列的最大长度。
4、等待客户请求的到来;当请求到来后,接受连接请求,返回一个新的对应于此连接的套接字(accept)。
接下来,需要调用accept函数等待并接受客户的连接请求。因为作为服务器端,它需要不断的等待客户端的连接请求的到来,所以设计成一个死循环。当客户端有请求时,该函数接受请求建立连接,同时返回一个相对于当前这个新连接的一个套接字描述符,保存于sockConn变量中,然后利用这个套接字就可以与客户端进行通信了,而我们先前的套接字仍继续监听客户端的连接请求。
5、用返回的套接字和客户端进行通信(send/recv)
可以调用send函数向客户端发送数据,注意这里使用的套接字是已建立连接的那个套接字:sockConn,而不是用于监听的那个套接字:addrSrv。使用recv函数从客户端接收数据。
6、返回等待另一个客户请求
7、关闭套接字
上面实现的服务器端的程序,下面是客户端程序实现代码TCPClient.cpp:
Client
1//#include <windows.h>
2#include <Winsock2.h>
3#include <stdio.h>
4
5int main()
6{
7 WORD wVersionRequested;
8 WSADATA wsaData;
9 int err;
10
11 wVersionRequested = MAKEWORD(1, 1);
12 err = WSAStartup(wVersionRequested, &wsaData);
13 if( err != 0 )
14 {
15 return -1;
16 }
17 if( LOBYTE(wsaData.wVersion) != 1 ||
18 HIBYTE(wsaData.wVersion) != 1)
19 {
20 WSACleanup();
21 return -1;
22 }
23
24 SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
25 SOCKADDR_IN addSrv;
26 addSrv.sin_addr.S_un.S_addr = inet_addr( "127.0.0.1" );
27 addSrv.sin_family = AF_INET;
28 addSrv.sin_port = htons(6000);
29
30 connect(sockClient, (SOCKADDR*)&addSrv, sizeof(SOCKADDR));
31
32 char receiveBuf[1000];
33 recv(sockClient, receiveBuf, 1000, 0);
34 printf("%s\n", receiveBuf);
35
36 send(sockClient, "This is lantionzy", strlen("This is lantionzy")+1, 0);
37 closesocket(sockClient);
38
39 WSACleanup();
40}
对于客户端来说,它不需要邦定,可以直接连接服务器端。
首先运行服务器端程序,然后再运行客户端程序,可以看到客户端收到了服务器端返回的信息:Welcome 127.0.0.1 to http://www.cppblog.com/lantionzy,而服务器端收到了客户端发送的信息:This is lantionzy。
剖析网络编程(2)-- 基于UDP的的网络应用程序 剖析网络编程(3)-- 基于TCP/UDP网络编程应注意的几个地方