原文:
WSA函数:https://blog.csdn.net/wangjiechen/article/details/52172885?ops_request_misc=&request_id=&biz_id=102&utm_term=windows%20socket%20api&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-52172885
简单TCP程序教程:
https://www.cnblogs.com/DOMLX/p/9601511.html
WSA函数部分做了一些排版和补充,改善了一下可读性。
TCP程序可以直接跑一下帮助理解TCP是怎么工作的,想了解详情请看原文。
Windows Socket API
1.WSAStartup
int WSAStartup(WORD wVersionRequested(指定socket版本), LPWSADATA lpWSAData);
使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
参数 & 返回值
-
第一个参数:
指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
-
第二个参数:
操作系统利用第二个参数返回请求的Socket的版本信息。
-
返回值:
若函数执行成功则返回0。
当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
涉及数据类型
LPWSADATA
:
指向WSADATA数据结构体指针,接收Windows Socket的实现细节。
WSADATA
:
这个结构被用来存储被WSAStartup函数调用后返回的Windows Sockets数据。它包含Winsock.dll执行的数据。
实例
实例:假如一个程序要使用2.1版本的Socket,那么程序代码如下
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
2.WSACleanup
int WSACleanup (void);
应用程序在完成对请求的Socket库的使用后,要调用WSACleanup
函数来解除与Socket库的绑定并且释放Socket库所占用的系统资源。
3.socket
SOCKET socket(int af(协议组), int type(套接字类型), int protocol(指定具体协议));
用来创建一个能够进行网络通信的套接字。
参数 & 返回值
-
第一个参数:
指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置PF_INET;
-
第二个参数:
指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM;
-
第三个参数:
指定应用程序所使用的通信协议。(如果里边就一种协议那就0)
-
返回值:
如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。
涉及数据类型
SOCKET
是套接字描述符的数据类型,用来标识一个套接字,而socket是创建套接字的函数。
套接字描述符:
- 是一个整数类型的值。
- 每个进程的进程空间里都有一个套接字描述符表,存放着套接字描述符和套接字数据结构的对应关系。
- 该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。
- 每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
实例
//协议组:AF_INET
//套接字类型:SOCK_STREAM,即TCP传输
//某协议中只有一种特定类型时,protocol参数设置为0
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
4.closesocket
int closesocket(SOCKET s);
closesocket函数用来关闭一个描述符为s套接字。
参数 & 返回值
- 参数:
要关闭的描述符s。
- 返回值:
closesocket函数如果执行成功就返回0,否则返回SOCKET_ERROR。
机制:
由于每个进程中都有一个套接字描述符表,表中的每个套接字描述符都对应了一个位于操作系统缓冲区中的套接字数据结构,因此有可能有几个套接字描述符指向同一个套接字数据结构。
套接字数据结构中专门有一个字段存放该结构的被引用次数,即有多少个套接字描述符指向该结构。
当调用closesocket函数时,操作系统先检查套接字数据结构中的该字段的值,如果为1,就表明只有一个套接字描述符指向它,因此操作系统就先把s在套接字描述符表中对应的那条表项清除,并且释放s对应的套接字数据结构;
如果该字段大于1,那么操作系统仅仅清除s在套接字描述符表中的对应表项,并且把s对应的套接字数据结构的引用次数减1。
5.send
int send(SOCKET s(指定发送端套接字), const char FAR *buf(指定一块缓冲区), int len(指定要发送的字节数), int flags );
使用send函数向TCP连接的另一端发送请求。
参数
-
第一个参数:
指定一个发送端套接字描述符;
-
第二个参数:
指明一个存放应用程序要发送数据的缓冲区;
-
第三个参数:
指明实际要发送的数据的字节数;
-
第四个参数:
一般置0。
flag的取值和对应的意义:
0: 与write()无异
MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息
send函数的执行流程
这里是讲同步的情况。
当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲区的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完;
-
如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len,如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里。
(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
返回值
如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。
如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。
每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回SOCKET_ERROR。
在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
6.recv
int recv(SOCKET s(指定一个接收端套接字), char FAR *buf(指定一块接收端缓冲区), int len(缓冲区长度), int flags);
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
参数
-
第一个参数:
指定接收端套接字描述符;
-
第二个参数:
指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
-
第三个参数:
指明buf的长度;
-
第四个参数:
一般置0
flags取值有:
0:常规操作,与read()相同
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息
MSG_PEEK:可以查看可读的信息,在接收数据后不会将这些数据丢失
MSG_WAITALL:通知内核直到读到请求的数据字节数时,才返回。
recv的执行流程 & 返回值
这里只描述同步recv函数的执行流程。
当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR.
如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。
如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
7.bind
int bind(SOCKET sockfd(要绑定的socket的描述符), const struct sockaddr FAR *name(该指针指定一个sockaddr结构), int namelen(被指定的结构的大小));
当创建了一个Socket以后,套接字数据结构中有一个默认的IP地址和默认的端口号。
服务程序调用bind
函数来给其绑定一个IP地址和一个特定的端口号。客户程序一般不必调用bind
函数来为其Socket绑定IP地址和断口号。
参数
-
第一个参数:
指定待绑定的Socket描述符;
-
第二个参数:
指定一个sockaddr结构,该结构是这样定义的:
struct sockaddr {
u_short sa_family;//协议族
char sa_data[14]; //socket地址
};
sa_family
指定地址族,对于TCP/IP协议族的套接字,给其置AF_INET。
当对TCP/IP协议族的套接字进行绑定时,我们通常使用另一个地址结构:
struct sockaddr_in {
short sin_family; //协议族,置AF_INET
u_short sin_port; //端口号
struct in_addr sin_addr; //sin_addr中只有一个唯一字段s_addr,表示IP地址
char sin_zero[8];
};
sin_family
置AF_INET
sin_port
指明端口号
sin_addr
结构体中只有一个唯一的字段s_addr
,表示IP地址,该字段是一个整数,一般用函数inet_addr()
把字符串形式的IP地址转换成unsigned long型的整数值后再置给s_addr
。
有的服务器是多宿主机,至少有两个网卡,那么运行在这样的服务器上的服务程序在为其Socket绑定IP地址时可以把htonl(INADDR_ANY)
置给s_addr
,这样做的好处是不论哪个网段上的客户程序都能与该服务程序通信;如果只给运行在多宿主机上的服务程序的Socket绑定一个固定的IP地址,那么就只有与该IP地址处于同一个网段上的客户程序才能与该服务程序通信。
htonl函数的内部实现原理是这样,先判断主机是什么模式存储,如果是大端模式,就跟网络字节序一致,直接返回参数即可,如果是小端模式,则把形参转换成大端模式存储在一个临时参数内,再把临时参数返回;
详见:https://blog.csdn.net/zhangdawei5A504/article/details/45747937
我们用0来填充sin_zero
数组,目的是让sockaddr_in
结构的大小与sockaddr
结构的大小一致。
实例
下面是一个bind
函数调用的例子:
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; //协议族为AF_INET
saddr.sin_port = htons(8888); //端口号8888
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(ListenSocket,(struct sockaddr *)&saddr,sizeof(saddr));
8.listen
int listen( SOCKET s(将s置于监听状态), int backlog(最多可以容纳的客户连接请求数));
服务程序调用listen函数使其流套接字s处于监听状态。
处于监听状态的流套接字s将维护一个客户连接请求队列,该队列最多容纳backlog个客户连接请求。
假如该函数执行成功,则返回0;如果执行失败,则返回SOCKET_ERROR。
9.accept
SOCKET accept(SOCKET s(指定一个处于监听状态的流套接字), struct sockaddr FAR *addr(用于返回新创建的套接字的地址结构), int FAR *addrlen(用于返回新创建的套接字的地址结构的长度));
服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道。
参数
-
第一个参数:
指定处于监听状态的流套接字
-
第二个参数:
操作系统利用第二个参数来返回新创建的套接字的地址结构
-
第三个参数:
操作系统利用第三个参数来返回新创建的套接字的地址结构的长度
返回值
如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;
如果失败就返回INVALID_SOCKET。
实例
一个调用accept
的例子:
struct sockaddr_in ServerSocketAddr;
int addrlen;
addrlen=sizeof(ServerSocketAddr);
ServerSocket=accept(ListenSocket,(struct sockaddr *)&ServerSocketAddr,&addrlen);
10.connect
int connect(SOCKET sockfd, const struct sockaddr FAR *addr(传入参数,指定服务器端地址信息,含IP地址和端口号), int namelen(sizeof(addr)大小));
客户程序调用connect函数会引发三次握手,使客户的socket与监听于addr所指定的计算机的特定端口上的服务Socket进行连接。
connect详解:https://blog.csdn.net/junjun150013652/article/details/37966901
参数
- sockdf:
socket文件描述符 - addr:
传入参数,指定服务器端地址信息,含IP地址和端口号 - addrlen:
传入参数,传入sizeof(addr)大小
返回值
如果连接成功,connect返回0;
如果失败则返回SOCKET_ERROR。
实例
struct sockaddr_in daddr;
memset((void *)&daddr,0,sizeof(daddr));
daddr.sin_family=AF_INET;
daddr.sin_port=htons(8888);
daddr.sin_addr.s_addr=inet_addr("133.197.22.4");
connect(ClientSocket,(struct sockaddr *)&daddr,sizeof(daddr));
一个简单的TCP连接例子
分别对服务器程序和客户程序建立一个项目,连接时先运行服务器程序再运行客户端程序。
1.服务器程序:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
using namespace std;
#pragma comment(lib, "ws2_32.lib")
void main() {
WSADATA wsaData;
int port = 5099; //端口号
char buf[] = "Server: 我是服务器。";
//套接字版本号为2.2
//调用成功会返回0
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cout << "Failed to load Winsock" << endl;
return;
}
//创建用于监听的套接字
//AF_INET是IPv4网络协议的套接字类型
//sock_stream 是有保障的面向连接的SOCKET
//某协议中只有一种特定类型时,protocol参数设置为0
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port); //1024以上的端口号
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
if (retVal == SOCKET_ERROR) {
cout << "Failed bind: " << WSAGetLastError() << endl;
return;
}
if (listen(sockSrv, 10) == SOCKET_ERROR) {
printf("Listen failed:%d", WSAGetLastError());
return;
}
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
//等待客户请求到来
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
if (sockConn == SOCKET_ERROR) {
printf("Accept failed:%d", WSAGetLastError());
//break;
}
printf("Accept client IP:[%s]\n", inet_ntoa(addrClient.sin_addr));
//发送数据
int iSend = send(sockConn, buf, sizeof(buf), 0);
if (iSend == SOCKET_ERROR) {
printf("send failed");
// break;
}
char recvBuf[100];
memset(recvBuf, 0, sizeof(recvBuf));
//接收数据
recv(sockConn, recvBuf, sizeof(recvBuf), 0);
printf("%s\n", recvBuf);
closesocket(sockConn);
closesocket(sockSrv);
WSACleanup();
}
2.客户端程序
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#pragma comment(lib, "ws2_32.lib")
void main()
{
//加载套接字
WSADATA wsaData;
char buff[1024];
memset(buff, 0, sizeof(buff));
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to load Winsock");
return;
}
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(5099);
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//创建套接字
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == sockClient) {
printf("Socket() error:%d", WSAGetLastError());
return;
}
//向服务器发出连接请求
if (connect(sockClient, (struct sockaddr*) & addrSrv, sizeof(addrSrv)) == INVALID_SOCKET) {
printf("Connect failed:%d", WSAGetLastError());
return;
}
else
{
//接收数据
recv(sockClient, buff, sizeof(buff), 0);
printf("%s\n", buff);
}
//发送数据
char buffSend[] = "Client: 我是客户。";
send(sockClient, buffSend, strlen(buffSend) + 1, 0);
printf("%d", strlen(buffSend) + 1);
//关闭套接字
closesocket(sockClient);
WSACleanup();
system("pause");
}
运行结果
这样就显示TCP连接成功了。