在前面的文章中有一篇讲到了命名管道通信,它是创建一根管道来进行进程之间或网络之间通信的。但是它有些缺陷,比如说效率较低等。而从这篇文章开始将介绍socket编程。socket是通过TCP,UDP,IP协议来进行通信,效率较高。本文先介绍TCP的服务端和客户端。
TCP是面向连接的,它在进行通信之前呢,需要双方先进行沟通,然后才能进行通信。而且TCP是以数据流的方式进行数据传递,会自动的进行拆包和组包的过程。所以呢,TCP的连接是比较可靠的,但是它的传输速度也因此相对较慢。接下来看下如何在代码中实现TCP的通信。
在windows中,要想进行socket网络操作,必须包含一个名叫做WinSock2.h(或者WinSock.h),如果包含的是WinSock2.h则必须在windows.h之前,否则会产生一些重定义的编译错误。包含完头文件之后,还要链接一个库文件ws2_32.lib,完成之后,我们就可以开始进行TCP服务端和客户端的编写了。
先来看服务端。首先,要进行网络操作,我们先要进行一下网络环境的初始化。WSAStartup函数就是用来初始化网络环境的。其声明如下:
int WSAStartup(
WORD wVersionRequested, //版本号,一般使用2.2版本
LPWSADATA lpWSAData //WSAData地址
);
上面函数的第二个参数,接收一个WSAData结构的指针,该结构呢,里边包含了版本号,我们传递的版本号会对该结构里边的版本号进行初始化。
初始化完成之后,我们需要创建一个socket(套接字),这个套接字相当于我们前面讲到的管道,用于客户端和服务端的连接。调用socket函数我们可以创建一个套接字,声明如下:
SOCKET socket(
int af, //IP协议簇
int type, //套接字类型,TCP应该用SOCK_STREAM
int protocol //协议
);
其实,socket也是一个内核对象,但是它没有内核对象所拥有的明显标志,安全属性。
创建好套接字后呢,我们需要告诉操作系统需要在哪个地址和端口上进行网络操作,相当于管道通信中绑定到标准输入输出口上。绑定的时候,需要有一个SOCKADDR_IN这个结构体,声明如下:
struct sockaddr_in{
short sin_family; //协议簇
unsigned short sin_port; //端口
struct in_addr sin_addr; //ip地址
char sin_zero[8]; //为了设置和SOCKADDR结构等长的补充字节
};
还有一个SOCKADDR结构和上面这个的功能完全一样,但是SOCKADDR这个结构里边只有两个成员,一个是协议簇,一个是14个字节的char数组,为了让我们更好的编写代码,于是将char数组拆解成SOCKADDR_IN 中后三个成员。初始化完端口,地址等信息后,需要调用bind函数,来完成绑定操作,声明如下:
int bind(
SOCKET s, //我们创建的那个socket
const struct sockaddr FAR *name, //sockaddr结构指针
int namelen //sockaddr长度
);
绑定之后,我们还需要调用listen函数来进行监听操作,这个操作呢,就相当于门卫一样了,如果有人来,就告诉你一声,这就是监听。该函数声明如下:
int listen(
SOCKET s, //我们创建的socket
int backlog //最大连接的队列长度
);
第二个参数backlog呢,我们一般不要给的太大,这就好比你去交电费,还要进行排队等候,如果排队的人多了,这就会给你留下不好的体验,因此随便给个10,100的就行了。
SOCKET accept(
SOCKET s, //我们监听的那个socket
struct sockaddr FAR *addr, //我们需要传递一个sockaddr的地址,用于保存客户端的地址
int FAR *addrlen //sockaddr的长度指针
);
接完客之后,我们就可以进行通信了,需要调用recv和send两个函数来进行收发数据,它们的声明如下:
int recv(
SOCKET s, //客户端的socket
char FAR *buf, //接收的缓冲区
int len, //缓冲区的大小
int flags //标志位,一般为0
);
int send(
SOCKET s, //客户端的socket
const char FAR *buf, //发送数据的缓冲区
int len, //缓冲区的大小
int flags //标志位,一般为0
);
当我们传输完数据后,应该调用WSACleanup和closesocket来进行关闭网络环境和套接字。声明如下:
int WSACleanup (void);
int closesocket(
SOCKET s //要关闭的套接字
);
下面附上服务端的示例代码:
#include
#include // 必须包含windwos.h之前
#include
#pragma comment(lib,"ws2_32.lib")
#define PORT 6000
DWORD WINAPI clientProc(LPARAM lparam)
{
SOCKET sockClient = (SOCKET)lparam;
char buf[1024];
while (TRUE)
{
memset(buf, 0, sizeof(buf));
// 接收客户端的一条数据
int ret = recv(sockClient, buf, sizeof(buf), 0);
//检查是否接收失败
if (SOCKET_ERROR == ret)
{
printf("socket recv failed\n");
closesocket(sockClient);
return -1;
}
// 0 代表客户端主动断开连接
if (ret == 0)
{
printf("client close connection\n");
closesocket(sockClient);
return -1;
}
// 发送数据
ret = send(sockClient, buf, strlen(buf), 0);
//检查是否发送失败
if (SOCKET_ERROR == ret)
{
printf("socket send failed\n");
closesocket(sockClient);
return -1;
}
}
closesocket(sockClient);
return 0;
}
bool InitNetEnv()
{
// 进行网络环境的初始化操作
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
printf("WSAStartup failed\n");
return false;
}
return true;
}
int main(int argc, char * argv[])
{
if (!InitNetEnv())
{
return -1;
}
// 初始化完成,创建一个TCP的socket
SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//检查是否创建失败
if (sServer == INVALID_SOCKET)
{
printf("socket failed\n");
return -1;
}
printf("Create socket OK\n");
//进行绑定操作
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET; // 协议簇为IPV4的
addrServ.sin_port = htons(PORT); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
addrServ.sin_addr.S_un.S_addr = INADDR_ANY; // ip地址,INADDR_ANY表示绑定电脑上所有网卡IP
//完成绑定操作
int ret = bind(sServer, (sockaddr *)&addrServ, sizeof(sockaddr));
//检查绑定是否成功
if (SOCKET_ERROR == ret)
{
printf("socket bind failed\n");
WSACleanup(); // 释放网络环境
closesocket(sServer); // 关闭网络连接
return -1;
}
printf("socket bind OK\n");
// 绑定成功,进行监听
ret = listen(sServer, 10);
//检查是否监听成功
if (SOCKET_ERROR == ret)
{
printf("socket listen failed\n");
WSACleanup();
closesocket(sServer);
return -1;
}
printf("socket listen OK\n");
// 监听成功
sockaddr_in addrClient; // 用于保存客户端的网络节点的信息
int addrClientLen = sizeof(sockaddr_in);
while (TRUE)
{
//新建一个socket,用于客户端
SOCKET *sClient = new SOCKET;
//等待客户端的连接
*sClient= accept(sServer, (sockaddr*)&addrClient, &addrClientLen);
if (INVALID_SOCKET == *sClient)
{
printf("socket accept failed\n");
WSACleanup();
closesocket(sServer);
delete sClient;
return -1;
}
//创建线程为客户端做数据收发
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)clientProc, (LPVOID)*sClient, 0, 0);
}
closesocket(sServer);
WSACleanup();
return 0;
}
接下来,看下客户端。
客户端比较简单,前面的部分和服务端都基本相同,在绑定操作上会有所差别。服务端绑定的IP地址是本机所有网卡的IP,而客户端只需要绑定一个即可,因为对客户端来说,我们只需连接指定的服务器。赋值完SOCKADDR_IN结构之后,服务端会调用bind函数,而客户端呢,需要调用connect函数,其声明如下:
int connect(
SOCKET s, //要进行连接的socket
const struct sockaddr FAR *name, //SOCKADDR结构地址
int namelen //SOKADDR大小
);
连接成功后,就可以和服务端进行通信了,调用recv和send来进行收发数据。需要注意的是,如果服务端程序先进行recv操作,则我们应该在客户端先进行send操作,若两个同时进行相同的操作的话,则会卡在当前的位置,因为recv和send都是阻塞型的函数。
当通信完之后,就可以关闭连接了。文章开头讲过,当客户端和服务端刚开始连接的时候呢,两者会先进行沟通,这个沟通需要3个步骤来完成,我们称之为3次握手,同样的关闭连接的时候,需要进行4个步骤来完成,我们称之为4次握手。如果你是粗暴型的,直接拔网线呢,它也会完成其中的两次步骤,作为应用层开发,并不需要深究其中的原理,若感兴趣,可自行查找资料。
下面附上客户端的示例代码:
#include
#include
#include
#pragma comment(lib,"ws2_32.lib")
#define PORT 6000
int main(int argc, char * argv[])
{
//初始化网络环境
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
printf("WSAStartup failed\n");
return -1;
}
// 初始化完成,创建一个TCP的socket
SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sServer == INVALID_SOCKET)
{
printf("socket failed\n");
return -1;
}
//指定连接的服务端信息
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(PORT);
//客户端只需要连接指定的服务器地址,127.0.0.1是本机的回环地址
addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 服务器Bind 客户端是进行连接
int ret = connect(sServer,(SOCKADDR*)&addrServ,sizeof(SOCKADDR));//开始连接
if (SOCKET_ERROR == ret)
{
printf("socket connect failed\n");
WSACleanup();
closesocket(sServer);
return -1;
}
//连接成功后,就可以进行通信了
char szBuf[1024];
memset(szBuf,0,sizeof(szBuf));
sprintf_s(szBuf,sizeof(szBuf),"Hello server");
//当服务端是recv的时候,客户端就需要send,若两端同时进行收发则会卡在这里,因为recv和send是阻塞的
ret = send(sServer, szBuf, strlen(szBuf), 0);
if (SOCKET_ERROR == ret)
{
printf("socket send failed\n");
closesocket(sServer);
return -1;
}
ret = recv(sServer, szBuf, sizeof(szBuf), 0);
if (SOCKET_ERROR == ret)
{
printf("socket recv failed\n");
closesocket(sServer);
return -1;
}
printf("%s\n",szBuf);
//关闭连接
closesocket(sServer);
WSACleanup();
return 0;
}