您的位置:首页 > 理论基础 > 计算机网络

网络编程(基于winsocket)-- 常用函数介绍(一)

2017-06-28 10:09 579 查看
本文主要记录网络编程中经常用到的函数,作为学习的笔记                                                                                                                                                                                                      
     

1、winsocket地址结构

一般的sockaddr地址结构:

typedef struct sockaddr {
u_short sa_family; //协议族
CHAR sa_data[14]; //IP地址和端口号
} SOCKADDR, *PSOCKADDR, FAR *LPSOCKADDR;

除了以上的不常用的地址结构外,还有一个常用的地址结构:

typedef struct in_addr {
union {
struct { UCHAR s_b1, s_b2, s_b3, s_b4; } S_un_b;
struct { USHORT s_w1, s_w2; } S_un_w;
ULONG S_addr;
} S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

typedef struct sockaddr_in {
short sin_family; //协议族
u_short sin_port; //端口号
IN_ADDR sin_addr; //IP地址
CHAR sin_zero[8]; //填充数据
} SOCKADDR_IN, *PSOCKADDR_IN;

以上两种地址结构用法相似,作为相同;相比于sockaddr,sockaddr_in的数据段更为详细,可以使用sockaddr_in来初始化然后强制转化为sockaddr,然后将其作为套接字函数的参数。sockaddr_in为了保证与sockaddr相同的长度,还增加了8字节的填充数据。

此外winsocket还提供了对ipv6的支持。

typedef struct in6_addr {
union {
UCHAR Byte[16];
USHORT Word[8];
} u;
} IN6_ADDR, *PIN6_ADDR, FAR *LPIN6_ADDR;

typedef struct sockaddr_in6 {
ADDRESS_FAMILY sin6_family; // ipv6协议族
USHORT sin6_port; // 端口号
ULONG sin6_flowinfo; // ipv6流信息
IN6_ADDR sin6_addr; // ipv6地址
union {
ULONG sin6_scope_id;
SCOPE_ID sin6_scope_struct;
};
} SOCKADDR_IN6_LH, *PSOCKADDR_IN6_LH, FAR *LPSOCKADDR_IN6_LH;

除了以上地址类型外,winsocket2还增加了addrinfo结构用于描述地址信息,该结构在getaddrinfo函数中使用,以链表的形式保存地址信息

addrinfo结构的结构如下:

typedef struct addrinfo
{
int ai_flags; // getaddrinfo函数的调用选项
int ai_family; // 地址族
int ai_socktype; // 套接字类型
int ai_protocol; // 协议
size_t ai_addrlen; // ai_addr指向的sockaddr结构缓冲区字节长度
char * ai_canonname; // 主机的正规名称
_Field_size_bytes_(ai_addrlen) struct sockaddr * ai_addr; // 以sockaddr结构描述的地址信息
struct addrinfo * ai_next; // 指向下一个addrinfo结构
}ADDRINFOA, *PADDRINFOA;
参数1(ai_flags):指定如何处理地址和名字,常见的参数值如下:
AI_PASSIVE 0X00000001 // 套接字地址将用于监听绑定
AI_CANONNAME 0x00000002 // 返回一个规范名
AI_NUMERICHOST 0x00000004 // 以数字形式返回主机地址
AI_NUMERICSERV 0x00000008 // 以端口号返回服务
AI_ALL 0x00000100 // 查找IPv4和IPv6地址
AI_ADDRCONFIG 0x00000400 // 查询配置的地址类型(IPv4或是IPv6)
AI_V4MAPPED 0x00000800 // 如果没有找到IPv6地址,则返回映射到IPv6格式的IPv4地址
参数2(ai_family):指定地址族,常见的地址族如下:

AF_INET 2 //IPv4:
AF_INET6 23 //IPv6
AF_UNSPEC 0 //协议无关
参数3(ai_socktype):指明套接字的类型,套接字类型如下:

SOCK_STREAM 1 /* 流式套接字 (TCP)*/
SOCK_DGRAM 2 /* 数据包套接字(UDP)*/
S
ce3a
OCK_RAW 3 /* 原始套接字 */
SOCK_RDM 4 /* 可靠传输套接字*/
SOCK_SEQPACKET 5 /* 在UDP的基础上提供伪数据包 *
参数4(ai_protocol):指明传输协议,该参数取决于af、type参数类型

IPPROTO_TCP //TCP协议,使用条件:af是AF_INET or AF_INET6;type是SOCK_STREAM
IPPROTO_UDP //UDP协议,使用条件:af是AF_INET or AF_INET6;type是SOCK_DGRAM)
IPPROTO_RM //PGM(Pragmatic General Multicast,实际通用组播协议)协议,使用条件,af是AF_INET 、type是SOCK_RDM
参数5(ai_addrlen):指定addrinfo结构的长度

参数6 (ai_canonname):主机的规范名称

参数7 (ai_addr):指向sockaddr结构

指向 sockaddr 结构的指针。每个返回的addrinfo结构中的ai_addr成员指向一个填充的套接字地址结构。

参数8 (ai_next):指向下一个addrinfo结构

addrinfo结构主要用于getaddrinfo()中,getaddrinfo函数提供了一种与协议无关的地址获取和表示方式,通过getaddrinfo获取到的addrinfo结构中的内容都是以网络字节顺序表示。

INT
WSAAPI
getaddrinfo(
_In_opt_ PCSTR pNodeName,
_In_opt_ PCSTR pServiceName,
_In_opt_ const ADDRINFOA * pHints,
_Outptr_ PADDRINFOA * ppResult
);
参数1:点分十进制形式的IP地址字符串,const char *

参数2:端口号或是服务名称,http、ftp,const char*

参数3:指向addrinfo结构的指针

参数4:返回一个指向addrinfo结构体链表的指针

返回值:0,成功;非0,失败;

2、地址转换
利用inet_addr()函数将点分十进制地址转换为无符号4字节的整数地址

失败返回INADDR_NONE,证明是一个非法地址。

利用inet_ntoa()函数将找整数性抵制装换为点分十进地址

在80x86的架构下,inet_addr()函数按照大端模式读取内存中的四字节的IP地址,同样inet_ntoa()函数按照小段模式读取内存中四字节的IP地址。

3、网络字节和主机字节的转换

htonl():将主机字节顺序从u_long转换为网络字节

htons():将主机字节顺序从u_short转换为网络字节

ntohl():将u_long从网络字节转换为主机字节

ntohs():将u_short从网络字节转换为主机字节

4、流式(TCP)套接字交互模型



5、函数介绍

(1)WSAStartup():初始化函数

int WSAStartup(
_In_ WORD wVersionRequested, //双字节版本号,低字节为主版本号,高字节为修订版本号
_Out_ LPWSADATA lpWSAData //<span style="color: rgb(51, 51, 51); line-height: 25px;"><span style="font-family:SimSun;">指向wasdata数据结构的指针,一般都是用来接收winsock实现的细节</span></span>
);
编程示例:

WSADATA wsaData;
int iResult;
//初始化winsocket
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
printf("初始化出错:%d\n", iResult);
return 1;
}

(2)socket():创建套接字函数

SOCKET WSAAPI socket(
_In_ int af, //套接字的通信地址族
_In_ int type, //套接字类型
_In_ int protocol //传输协议
);
参数1:指明通信地址族。常用的地址族如下:
AF_UNSPEC //未指明
AF_INET //IPv4
AF_NETBIOS //NETBIOS地址簇
AF_INET6 //IPv6
AF_IRDA //Infrared Data Association (IrDA)地址簇
AF_BTM //Bluetooth
参数2:指明套接字类型,常见的套接字类型如下:
SOCK_STREAM //流套接字,使用TCP协议
SOCK_DGRAM //数据报套接字,使用UDP协议
SOCK_RAW //原始套接字
SOCK_RDM //提供可靠的消息数据报文
SOCK_SEQPACKET //在UDP的基础上提供了伪流数据包
参数3:指明传输协议,该参数取决于af、type参数类型

IPPROTO_TCP //TCP协议,使用条件:af是AF_INET or AF_INET6;type是SOCK_STREAM
IPPROTO_UDP //UDP协议,使用条件:af是AF_INET or AF_INET6;type是SOCK_DGRAM)
IPPROTO_RM //PGM(Pragmatic General Multicast,实际通用组播协议)协议,使用条件,af是AF_INET 、type是SOCK_RDM
该参数默认指定为0,系统会默认选择对应于指定协议族(参数1)和套接字类型(参数2)对应的默认协议

返回值:函数不出错,返回socketet描述符;否则返回INVALID_SOCKET

在客户端进程中,该套接字用于数据传输;在服务端进程中该套接字只用于监听请求,不用于数据传输,accept()的返回值套接字用于数据传输。

编程示例:

SOCKET S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建基于TCP的IPv4服务端
if (S == INVALID_SOCKET)
{
printf("出错:%d\n", WSAGetLastError());
WSACleanup();
return 1;
}
(3)bind():服务端地址与socket关联函数

int WSAAPI bind(
_In_ SOCKET s, //socket()函数返回的描述符
_In_reads_bytes_(namelen) const struct sockaddr FAR * name, //地址参数
_In_ int namelen //地址结构的大小
);
参数1:socket()函数返回的套接字描述符

参数2:指向sockaddr结构的指针。对于TCP协议通常使用sockaddr_in(IPv4)或sockaddr_in6(IPv6)对地址进行赋值,然后将指针强制类型转换为指向sockaddr结构的指针。

参数3:地址结构大小

返回值:成功返回1,否则,返回0

编程示例:

SOCKET S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建基于tcp的ipv4
if (S == INVALID_SOCKET)
{
printf("出错:%d\n", WSAGetLastError());
WSACleanup();
return 1;
}
sockaddr_in addr; //sockaddr_in地址结构
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //IP地址设置为“127.0.0.1”;inet_addr()函数将点分十进制转换为大端模式的4字节整形数
addr.sin_family = AF_INET;
addr.sin_port = htons(12345); //将端口号转换为大端模式的整形数
iResult = bind(S, (sockaddr*)&addr, sizeof(addr)); //为套接字绑定地址和端口号
if (iResult == SOCKET_ERROR)
{
printf("绑定失败:%d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return 1;
}
这里涉及到两个问题:

(1) 具有多个Internet地址(多网卡)的主机服务端进程如何为客户提供服务

sockaddr或sockaddr_in结构指定的地址类型可以是两种:

1)常规地址,包含一个特定的主机地址和端口号。

2)通配地址,当应用程序不关心赋予给它的特定的主机地址,那么可以将sockaddr赋值为任意地址,即主机地址地址为INADDR_ANY和端口号为0。此时应用程序可以

将任意可用的网络接口都将与这个套接字关联起来,那么可以接受

winsocket地址类型:

INADDR_ANY(ULONG)0x00000000 //任意
INADDR_LOOPBACK 0x7f000001 //回环地址:127.0.0.1
INADDR_BROADCAST(ULONG)0xffffffff //广播地址
INADDR_NONE 0xffffffff //错误地址返回值
u_long a = inet_addr("127.0.0.1");
u_long b = htonl(INADDR_LOOPBACK); //这两句的返回值相等

(2) 客户端是否需要bind()操作

客户端需要与服务端主动建立连接,那么就需要知道服务端进程的IP地址和端口号,所以服务端需要bind()操作绑定IP地址和端口号,而对于客户端来说,如果调用bind()绑定地址和端口号,可能造成端口发生冲突,造成通信无法进行。实际上,在客户端调用connect()或是sendto()发送数据前,系统会从当前未使用的端口号随机中选择一个,隐式调用bind()来实现客户套接字与本地地址的关联。

客户端和服务端在通信时,双方就已经确定了端口号和IP地址,对于服务端可以由bind函数绑定IP地址或是任意地址(INADDR_ANY),通信端口号可必须有程序员指定;对于客户端可以调用bind函数来绑定端口号作为通信端口,也可以由系统来指定通信端口,地址结构sockaddr中的端口号和IP地址必须和服务端一致。

(4)listen():连接监听函数

int WSAAPI listen(
_In_ SOCKET s, //套接字描述符
_In_ int backlog //接入连接的数量上限
);
参数backlog:指明可以等待进入连接的数量上限,

(5)connect():请求连接函数

int WSAAPI connect(
_In_ SOCKET s, //套接字描述符
_In_reads_bytes_(namelen) const struct sockaddr FAR * name, //sockaddr结构地址
_In_ int namelen //地址结构的长度
);
参数 name:为地址sockaddr结构体指针形变量,该结构体中保存着IP地址和端口号信息,可以通过sockaddr_in来强制转型得到,也可以通过addrinfo结构体来获得。

参数 namelen:地址结构体的长度,通常有sizeof(sockaddr_in)或是有addrinfo.ai_addrlen来获得。

connect()函数的连接请求到达服务端,直接进入请求队列,如果请求队列已满,那么该连接请求会被丢掉。

(6)accept()函数:获取客户端连接请求

该函数从连接请求队列中获取到一个连接请求

SOCKET WSAAPI accept(
_In_ SOCKET s, //已绑定的设置为“监听”状态的套接字描述符,该套接字只负责监听连接请求,不负责发送数据
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, //sockaddr地址指针
_Inout_opt_ int FAR * addrlen //地址指针的长度
);

accept()函数的返回值为另一个用于数据传输的套接字,称为已连接套接字,负责与本次连接的客户端通信。accept的操作实际上就是一个出队操作。在阻塞模式下,如果该函数未接收到连接请求,那么该函数将一直阻塞。

参数s:已经绑定了地址的socket()函数返回值套接字描述符

参数addr:指向sockaddr的地址的指针。当函数成功返回后,该地址被填充为连接的客户端地址和端口号。如果程序员对此不关心,可以将addr和addrlen设置为null

accept()函数返回的套接字类型为已连接套接字;

socket()函数返回的套接字类型为监听套接字。

(7)send():数据发送

int WSAAPI send(
_In_ SOCKET s, //套接字描述符
_In_reads_bytes_(len) const char FAR * buf, //指向发送缓冲区的指针
_In_ int len, //发送缓冲区的长度
_In_ int flags //标志位,一般为0
);

参数s:套接字描述符

在服务端为accept()函数返回的“已连接套接字”,该套接字用于数据接收和发送;另一个类型套接字为socket()函数返回的“监听套接字”,该套接字用于监听客户端的连接。

在客户端为socket()函数返回的套接字描述符,该套接字用于数据接受和发送。

注:

1、调用时send()函数首先比较发送数据的长度与send()函数缓冲区的长度,如果参数len超出send()函数缓冲区的长度,那么函数返回错误SOCKET_ERROR

2、如果len的长度小于send()函数发送缓冲区长度,那么send()函数将buf指向的缓冲区的数据copy到send()函数缓冲区,交由协议发送,此时调用玩send()函数不代表数据已经发送到目标端。如果发送完毕函数返回发送的字节数;如果发送失败,那么返回SOCKET_ERROR。

3、send()函数的返回值指示了实际发送的字节数,在默认情况下send()函数会一直阻塞到发送完所有数据为止。

(8)recv()函数:数据接收

int WSAAPI recv(
_In_ SOCKET s, //套接字描述符
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, //缓冲区指针
_In_ int len, //缓冲区大小
_In_ int flags //标志位,一般为0
);
参数s:套接字描述符

在服务端为accept()函数返回的“已连接套接字”,该套接字用于数据接收和发送;另一个类型套接字为socket()函数返回的“监听套接字”,该套接字用于监听客户端的连接。

 在客户端为socket()函数返回的套接字描述符,该套接字用于数据接受和发送。

注:

1、如果协议在传送s的发送缓冲区中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;

2、如果套接字描述符缓冲区没有数据或者协议正在接受数据,或者套接字缓冲区内数据不足以填满buf缓冲区,那么recv()函数就会一直等待(阻塞),直到协议把数据接收完毕

3、当协议把数据接收完毕,recv()函数就会把套接字描述符缓冲区的数据copy到参数2:buf缓冲区中;接收的数据可能大于buf缓冲区的大小,这是需要调用几次recv()函数来实现接收。

4、recv()函数只是完成了copy数据,真正数据接收是由协议来完成的

5、recv()函数返回值是实际接收的数据

这个函数涉及到一个问题:

1、发送端调用connect()函数连接到服务端、send()函数发送数据、recv函数接收数据。

2、服务端调用accept()函数接收客户端连接、调用recv()函数来接收来自客户端的数据、调用send()函数来发送数据给服务端。

3、服务器进车通过调用accept()函数获得了在该套接字上通信的客户端,同时客户端进程也通过调用connect()函数注册了服务端进程的套接字地址,这些地址信息在连接建立完成时保存在连接套接字指向的套接字结构上。

(9) shutdown()函数:关闭相关服务

int WSAAPI shutdown(
_In_ SOCKET s, //关闭连接的套接字描述符
_In_ int how // 关闭类型
);
参数s:关闭服务端或是客户端的连接

参数how:关闭类型如下:

SD_RECEIVE 0x00 //接收数据
SD_SEND 0x01 //发送数据
SD_BOTH 0x02 //接收和发送数据
此函数的功能通知对方不再进行参数2类型的操作,如客户端关闭类型为0,表示通知服务端,客户端不在接收数据,此时客户端还可以发送数据;

如果关闭失败,那么会返回SOCKET_ERROR
(10)closesocket():关闭连接

int WSAAPI closesocket(
_In_ SOCKET s //关闭套接字
);

(11)WSACleanup():释放资源
int WSAAPI WSACleanup(
void
);
调用closesocket()关闭连接后,如果不在使用socket DLL,调用WSACleanup()释放相关资源
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: