之前老师布置了不少关于任务,都是有关程序之间的通讯的。最开始只会读写文件,自然就用的是文件进行数据交流。文件交换数据有个问题就是一个程序读或者写的时候,文件被占用,另一个程序如果继续进行读取文件,就会报错。所以我就降低读写的频率,但是还是免不了两个程序同时操作文件的时候。后来学了数据库,虽然感觉牛逼一点了,但是感觉其实还是一个意思。后来学长给了一个socket+protobuf的动态链接库,用起来确实要比文件好,传输速度快,频率还高。用文件传,频率低了两个程序之间就会有延迟,换了socket延迟问题就没有了。
这两天事情不是很多,想起把这个东西看懂。学长给的那个是用C#写的,而且加了protobuf,很多东西看不懂,而且老师要求写的东西是MFC,要在MFC用这一套东西的话,还是得弄懂它,重写一遍。所以先从简单的开始,先把这个socket看懂。找了不少资料,有个网站的东西还不错,(http://c.biancheng.net/cpp/socket/)看了之后,懂了那么些意思。
我这里的用是TCP传输协议。
socket的基本操作
1. socket()函数
在Windows下创建socket,用socket函数来创建,原型如下:
SOCKET socket(int af, int type, int protocol)
- af (Address Family),也就是 IP 地址类型。有AF_INET 和 AF_INET6。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址。
- type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM。
- protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。简单理解就是TCP会保证数据传输的到达,UDP就是发出去就不管了。
一般情况下,有了af,type两个参数就可以确定创建套接字了。所以下面的代码,第三个参数就写成0。
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
2. bind()函数 connet()函数
1). bind()函数
创建套接字后服务器需要bind函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理。原型如下:
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);
- sock指前面创建的套接字
- addr 为 sockaddr 结构体变量的指针。我后面的代码写的却是sockaddr_in类型,然后又强制转换成sockaddr类型。这里为什么这么麻烦,可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。
- addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。
sockaddr_in sockAddr;//
memset(&sockAddr, 0, sizeof(sockAddr));//每个字节用0填充
sockAddr.sin_family = PF_INET;//设置IP为IPv4类型
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// 就写127.0.0.1 就可以了,指本地机,一般用来测试使用。
sockAddr.sin_port = htons(8888);//设置一个端口号 最好在1024~65536 之间分配端口号。
//sockaddr_in 结构体变量创建好后,要强制转换成SOCKADDR型,就可以将其与前面的套接字绑定起来。
if (-1 == bind(serSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)))
{
//这里这样写如果程序出错,可以得到错误编码。我当时就是IP地址的点写成了逗号,找了好久的错(哈哈)
DWORD err = GetLastError();
printf("bind error:%d\n", err);
return 0;
}
2). connect()函数
客户端在创建套接字之后就可以用connect()函数来建立连接了,原型为:
int connect(SOCKET sock, const struct sockaddr *serv_addr, int addrlen);
参数的说明和 bind() 相同
3. listen()函数
作为一个服务器,在调用socket()、bind()之后就会通过 listen() 函数可以让套接字进入被动监听状态。
原型如下:
int listen(SOCKET sock, int backlog);
- sock 为需要进入监听状态的套接字。
- backlog 为请求队列的最大长度。能存放多少个客户端请求。
4. accept() 函数
作为服务器套接字执行socket() bind() listen()后就处于于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen);
- 它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。
- accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
- accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来,就是客户端执行connect()。
//接收客户端请求
SOCKADDR clntAddr;//客户端地址
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(serSock, (SOCKADDR*)&clntAddr, &nSize);//返回后面用于与客户端通信用的新套接字。
5. 数据的接收和发送
服务器与客户端的套接字建立连接后,可以调用网络I/O进行读写操作了。
我这里就只用了send() recv() 原型如下:
int send(SOCKET sock, const char *buf, int len, int flags);
int recv(SOCKET sock, char *buf, int len, int flags);
- sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项,设置为0就可以了。
6. 关闭连接
要关闭服务器与客户端的连接。直接调用closesocket()。参数就是前面创建的套接字。
两个程序的源码
先执行服务器程序,然后执行客户端程序
- 服务器端
执行的函数依次是socket() bind() listen() accpet() send() closesocket()
#include "stdio.h"
#include "winsock2.h"
#pragma comment (lib, "ws2_32.lib")
//windows下这些函数写在了winsock2里,所以这里就照敲
int main()
{
//初始化DLL
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建套接字
SOCKET serSock = socket(PF_INET, SOCK_STREAM, 0);
if (serSock == INVALID_SOCKET)
{
printf("socket error!");
return 0;
}
//绑定套接字
sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr));//每个字节用0填充
sockAddr.sin_family = PF_INET;//IPv4
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// 具体IP地址
sockAddr.sin_port = htons(8888);//端口
if (-1 == bind(serSock, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR)))
{
DWORD err = GetLastError();
printf("bind error:%d\n", err);
return 0;
}
//监听状态
listen(serSock, 20);
//等待接收客户端请求
SOCKADDR clntAddr;
int nSize = sizeof(SOCKADDR);
SOCKET clntSock = accept(serSock, (SOCKADDR*)&clntAddr, &nSize);
//向客户端发送数据
char buffer[100] = { "Hello World!" };
send(clntSock, buffer, strlen(buffer) + sizeof(char), NULL);//发送数据
//关闭套接字
closesocket(serSock);
//终止DLL 使用
WSACleanup();
return 0;
}
- 客户端
执行的函数依次是socket() connect() recv() closesocket()
#include "stdio.h"
#include "winsock2.h"
#pragma comment (lib, "ws2_32.lib")
int main()
{
//初始化dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2,2);
if (WSAStartup(sockVersion,&wsaData) != 0)
{
return 0;
}
//创建套接字
SOCKET clnSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clnSocket == INVALID_SOCKET)
{
printf("socket error!");
return 0;
}
//绑定套接字 bind
sockaddr_in sockAddr;
sockAddr.sin_family = PF_INET;//IPv4
sockAddr.sin_port = htons(8888);//端口
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//具体IP地址
//connet 连接服务器
connect(clnSocket, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));
//接受服务器传回的数据
char szBuffer[MAXBYTE] = { 0 };
recv(clnSocket, szBuffer, MAXBYTE, NULL);
printf("Message form server :%s\n", szBuffer);
closesocket(clnSocket);
WSACleanup();
return 0;
}