套接字编程相关函数介绍以及案例
值-结果参数
在将套接字结构作为一个参数传递给套接字函数时,该结构的长度也作为一个参数来传递, 不过其传递方式取决于该结构传递方向:进程到内核、 内核到进程 从进程到内核传递套接字地址结构的函数有3个:bind、connect和sendto; 从内核到进程传递套接字地址结构的函数有4个:accept、recvfrom、getsockname 和getpeername; 这几个函数的大致定义为: //第二个参数为指向某个套接字地址结构的指针,第三个参数是该结构的整数大小 int functionName(int,const struct sockaddr*,socklen_t); //第二个参数为指向某个套接字地址结构的指针,第三个参数是指向该结构大小的 //整数变量的指针 int functionName(int,struct sockaddr *,socklen_t *); 当函数被调用是,结构体大小是一个值;而当函数返回时,结构体大小是一个结果; 所以这种类型的参数称为值-结构参数 在网络编程中,值-结果参数最常见的例子是返回套接字地址结构的长度
字节排序函数
考虑个16位整数,它由2个字节组成。内存中存储这两个字节有两种方法: -小端:低位字节放在内存的低地址端 -大端:低位字节放在内存的高地址端 主机字节序:给定系统所使用的字节序; 网络字节序:是TCP/IP中规定好的一种数据表示格式,采用大端排序法 在数据传输过程中,需要将主机字节序转换成网络字节序
#if defined(_WIN32)||defined(_WIN64)
#include <winsock.h>
#else
#include <netinet/in.h>
#endif
#if defined(WIN32) || defined(WIN64)
//返回网络字节序的值
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
//返回主机字节序
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);
#else
//返回网络字节序的值
uint32_t htonl(uint32_t __hostlong)
uint16_t htons(uint16_t __hostshort);
//返回主机字节序
uint32_t ntohl(uint32_t __netlong);
uint16_t ntohs(uint16_t __netshort);
#endif
在这些函数名字中,h代表host,n代表network,现在把s视为一个16位的值(端口号), l 视为32位的值(IPv4地址)
字节操纵函数
//将dest中指定数目len设为c,经常用该函数初始化一个套接字地址为0 void* memset(void* dest,int c,size_t len); //将指定数目的字节从源字节串移动目标字节串 void* memcpy(void* dest,const void* src,size_t nbytes); //比较任意两个字节串,相等返回0 int memcmp(const void* ptr1,const void* ptr2,size_t nbytes);
inet_aton、inet_addr和inet_ntoa函数
//将dest中指定数目len设为c,经常用该函数初始化一个套接字地址为0
void* memset(void* dest, int c, size_t len);
//将指定数目的字节从源字节串移动目标字节串
void* memcpy(void* dest, const void* src, size_t nbytes);
//比较任意两个字节串,相等返回0
int memcmp(const void* ptr1, const void* ptr2, size_t nbytes);
inet_pton和inet_ntop函数
这两个函数是随IPv6出现的新函数,对于IPv4地址和IPv6地址都适用。函数名中 的 p 和 n 分别代表地址表达格式和套接字地址结构中的二进制值
//window下<WS2tcpip.h>,linux下<arpa/inet.h>
//成功返回1,输入的不是有效值返回0,出错则为-1
int inet_pton(int family, const char* str, void* addrptr);
//成功返回指向结果的指针,出错则为nullptr
const char* inet_ntop(int family, const void* addrptr, char* str, size_t len);
这两个函数的family参数既可以是AF_INET也可以是AF_INET6; 第一个函数尝试转换由str所指的字符串,并通过addrptr存放二进制结果; 第二个函数进行相反的转换,len参数是目标存储单元(str)的大小,str不可以是一个 空指针,必须为它分配内存并指定其大小。调用成功时,这个指针就是该函数的返回值
//len的大小有如下定义:
//window下<WS2tcpip.h>,linux下<netinet/in.h>
#if defined(_WIN32)
# define INET_ADDRSTRLEN 22
# define INET6_ADDRSTRLEN 65
#else
# define INET_ADDRSTRLEN 16
# define INET6_ADDRSTRLEN 46
#endif
//函数使用方法
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
//旧版本:addr.sin_addr.s_addr = inet_addr(str);
不分协议的转换函数
#if defined(_WIN32) || defined(_WIN64)
#include <WS2tcpip.h>
#include <winsock.h>
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#endif
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
//若成功返回非空指针,出错返回nullptr
//scokaddr指向一个长度为addrlen的套接字地址结构
char* socket_ntop(const sockaddr* sa, socklen_t addrlen)
{
char portstr[8];
static char str[128];
switch (sa->sa_family)
{
case AF_INET:
{
sockaddr_in* sin = (sockaddr_in*)sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == nullptr)
return nullptr;
//将网络字节序转换成主机字节序
if (ntohs(sin->sin_port) != 0)
{
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return (str);
}
#ifdef IPV6
case AF_INET6:
{
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
str[0] = '[';
if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return (NULL);
if (ntohs(sin6->sin6_port) != 0)
{
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return (str);
}
return (str + 1);
}
}
#endif
return (NULL);
}
int main()
{
sockaddr_in Client; //服务器信息结构
Client.sin_port = htons(12345); //端口号
Client.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.x.x", &Client.sin_addr); //服务器IP地址
char* ptr = socket_ntop((sockaddr*)&Client, sizeof(Client));
cout << ptr << endl;
return 0;
}
//上述程序通过Windows与Linux测试
基本套接字编程
socket函数
在Linux下为了执行网络I/O,一个进程做的第一件事情就是调用socket函数, 指定期望的通信协议类型 而在Windows下需要先加载socket静态库在调用socket函数,Windows下该函 数与Linux下的socket函数行为一致 socket函数在成功时返回一个小的非负整数值,用来描述套接字,称为为套接 字描述符(sockfd)
//Linux下
#include <sys/socket>
int socket(int _family, int __type, int __protocol);
//windows下
#include <winsock.h>
typedef unsigned int SOCKET;
/*可以通过调用WSAGetLastError()检索特定的错误代码。*/
SOCKET socket(int _family, int __type, int __protocol);
参数说明: family:指明协议族,该参数也被称为协议域,常用的有AF_INET、AF_INET6; type:指明套接字类型,常用的有SOCK_DGRAM(UDP)、SOCK_STREAM(TCP); protocol:某个协议类型常值,或者设为0
famliy 说明 AF_INET IPv4协议 AF_INET6 IPv6协议 AF_LOCAL Unix域协议 AF_ROUTE 路由套接字 AF_KEY 密钥套接字
type 说明 SOCK_STREAM TCP套接字 SOCK_DGRAM UDP套接字 SOCK_SEQPACKET 有序分组套接字 SOCK_RAW 元素套接字
protocol 说明 IPPROTO_TCP TCP传输协议 IPPROTO_UDP Y IPPROTO_SCTP SCTP传输协议
connect函数
//TCP客户用connect函数来建立与TCP服务器的连接
//linux下
int connect(int, const struct sockaddr*, socklen_t);
//Windows下
using SOCKET = unsigned int;
int connect(SOCKET, const struct sockaddr*, int);
//第一个参数是由socket函数返回的套接字描述符,
//第二个参数指向套接字地址结构的指针
//第三个参数是该结构的大小、
//调用connect函数将会激发TCP三次握手的过程,而且仅在建立成功或出错时才返回
//连接建立成功时,connect函数返回非0值
connect()的返回值: - Linux下:成功返回0,失败返回-1,并置errno; - Windows下:成功返回0,失败返回SOCKET_ERROR,可以通过 WSAGetLastError()查看详细错误
bind函数
bind函数把一个本地协议地址赋予一个套接字。对于网际协议,协议地址是32位的 IPv4地址或128位的IPv6地址与16位的TCP或UDP端口号的结合,服务器在调用 listen() 之前,必须调用 bing(),将套接字与套接字结构绑定
//Linux下
#include <sys/socket.h>
int bind(int sockfd, const struct sockadr* myaddr, socklen_t addrlen);
//Windows下
#include <winsock.h>
int bind(SOCKET s, const sockaddr* name, int namelen);
第二个参数指向套接字结构地址的指针,第三个参数是该地址结构的长度, 调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指 定,还可以都不指定: - 服务器在启动时绑定它们众所周知的端口号,如果一个TCP客户或服务器未调用 bind绑定一个端口号,当调用connect或listen时,内核就会为相应的套接字 选择一个临时端口; - 进程可以把一个IP地址绑定到它的套接字上,不过这个IP地址必须属于其所在主机 的网络接口之一。对于TCP客户,这就为在该套接字上发生的IP数据报指定了源IP 地址。对于TCP服务器,这就限定了该套接字只接收那些目的地为这个IP地址的客 户连接。TCP客户通常不绑定IP地址,当连接套接字时,内核将根据所用外出网络 接口来选择源IP地址。如果TCP服务器没有把IP地址绑定到自己的套接字上,内核 就把客户发送的SYN包的目的IP地址作为服务器的源IP地址; 如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。对于IPv4来说, 通常指定INADDR_ANY作为通配地址。让内核来为套接字选择IP地址:
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind()的返回值: - Linux下:成功返回0,失败返回-1,并置errno; - Windows下:成功返回0,失败返回SOCKET_ERROR,可以通过 WSAGetLastError()查看详细错误;
listen 函数
listen函数仅由TCP服务器调用,它做两件事: - 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说, 它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接 的套接字转换成一个被动套接字。指示内核应接受该套接字的连接请求; - 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数; 本函数通常应该在调用socket和bind两个函数之后,并在调用accept函数之前调用 三次握手完成之后,但在服务器调用accept之前的数据应由服务器TCP排队, 最大数据量为相应已连接套接字的接收缓冲区大小
//Linux下
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//Windows下
int listen(SOCKET s, int backlog);
//backlog通常指定为SOMAXCONN,由该字段的具体实现设置最大连接个数
backlog参数理解
为了理解这个参数,首先需要认识到内核为任何一个给定的监听套接字维护两个队列: - 未完成连接队列,服务器正在等待完成相应的TCP三次握手的过程, 处于SYN_RCVD状态 - 已完成连接队列,每个已完成TCP三次握手过程的客户对应其中的一 项,这些套接字处于 ESTABLISHED 状态; 每当在未完成连接队列中创建一项时(新的连接到来),来自监听套接字的参数就复 制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手; 来自客户的SYN包到达时(即第一次握手),TCP在未完成连接队列中创建一个新 项,然后进行第二次握手(服务器的SYN响应,其中捎带对客户SYN包的确认回应 ACK)。这个新建的项一直保留在未完成连接队列中,直到第三次握手(客户对服 务器SYN包的确认回应ACK)到达或者该项超时为止。如果三次握手正常完成,该 项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成 队列中的对头项将返回给进程,如果该队列为空,那么进程将进入睡眠,直到TCP 在该队列中放入一项才唤醒它; 关于这两个队列的处理,一下几点需要考虑: - listen函数的backlog参数曾被规定为这两个队列总和的最大值; - 不要把backlog设为0,除非你不想服务器接收到任何客户的连接; - 在三次握手正常完成的前提下,未完成连接队列中的任何一项在其中的存留时 间是一个RTT,RTT的值取决于特定的客户与服务器; - backlog的最大值;
//backlog最大值: //linux下:允许环境变量LISTENQ覆写由调用者指定的值 void Listen(int sockfd,int backlog){ char* ptr; if((ptr=atoi("LISTENQ"))!=nullptr) backlog=atoi(ptr); if(listen(soclfd,backlog)<0) exit(-1); }
//backlog最大值:
//linux下:允许环境变量LISTENQ覆写由调用者指定的值
void Listen(int sockfd, int backlog) {
const char* ptr;
if ((ptr = getenv("LISTENQ")) != nullptr)
backlog = atoi(ptr);
if (listen(sockfd, backlog) < 0)
exit(-1);
}
//Windows下,通常指定为SOMAXCONN
if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
cout << WSAGetlastError() << endl;
}
accept函数
该函数由TCP服务器调用,从已完成连接队列中取出已经建立的客户连接, 然后把这个已经建立的连接返回给用户程序(套接字描述符)。如果已完 成连接队列为空,那么进程将被阻塞,直到新的连接到来
//Linux下
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* clientaddr, socklen_t* addrlen);
//Windows下
#include <winsock.h>
SOCKET accept(SOCKET sockfd, sockaddr* clientaddr, int* addrlen);
参数: sockfd:套接字描述符(监听套接字),该描述符置于监听状态; clientaddr:用于接收客户的协议地址(如果不感兴趣,可设为空指针); addrlen:clientaddr所指套接字地址结构长度(如果不感兴趣,可设为空指针);
返回值: - linux:如果成功,返回一个代表与客户的TCP连接的描述符(已连接套接字 描述符);失败,返回 -1; - Windows:如果成功,返回一个描述符;失败,返回INVALID_SOCKET;
监听套接字描述符与已连接套接字描述符的区别: - 一个服务器通常仅创建一个监听套接字,它在该服务器的生命周期内一直存在; - 内核为每个由服务器进程接受的客户连接 创建一个已连接套接字,当完成对 某个给定客户的服务时,相应的已连接套接字就被关闭(每次循环)
案例
//该代码在Windows与Linux下都能运行
#if defined(_WIN32) || defined(_WIN64)
#define _CRT_SECURE_NO_WARNINGS
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/socket.h>
#endif
#include <cstring>
#include <iostream>
#include <ctime>
#include <sstream>
using namespace std;
void error(int iResult, int sockfd)
{
#if defined(_WIN32) || defined(_WIN64)
if (iResult == SOCKET_ERROR)
{
#else
if (iResult < 0)
{
#endif
#if defined(_WIN32) || defined(_WIN64)
cout << "失败原因:" << WSAGetLastError() << endl;
closesocket(sockfd);
WSACleanup();
#else
close(sockfd);
#endif
exit(-1);
}
}
int main()
{
#if defined(_WIN32) || defined(_WIN64)
WSADATA wsd;
int Ret = 0;
//通知程序,加载socket库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
cout << "socket初始化失败: " << WSAGetLastError() << endl;
return -1;
}
#endif
int listenfd, connfd; //监听套接字,已连接套接字
#if defined(_WIN32) || defined(_WIN64)
int len;
#else
socklen_t len;
#endif
sockaddr_in serverAddr, clientAddr;
char buff[1024];
time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
int iResult = 0; //函数返回描述符
#if defined(_WIN32) || defined(_WIN64)
if (listenfd == INVALID_SOCKET)
{
cout << "失败原因:" << WSAGetLastError() << endl;
WSACleanup();
return -1;
}
#else
if (listenfd < 0)
{
exit(-1);
}
#endif
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(1234);
iResult = bind(listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr));
error(iResult, listenfd);
iResult = listen(listenfd, 100); //暂时设为20;
error(iResult, listenfd);
while (true)
{
len = sizeof(clientAddr);
connfd = accept(listenfd, (sockaddr*)&clientAddr, &len);
#if defined(_WIN32) || defined(_WIN64)
if (connfd == INVALID_SOCKET)
{
cout << "失败原因:" << WSAGetLastError() << endl;
closesocket(connfd);
WSACleanup();
return 1;
}
#else
if (connfd < 0)
{
exit(-1);
}
#endif
cout << "连接来自:"
<< inet_ntop(AF_INET, &clientAddr.sin_addr, buff, sizeof(buff))
<< ", port:" << ntohs(clientAddr.sin_port) << endl;
ticks = time(nullptr);
stringstream str;
str << buff << " " << ctime(&ticks);
#if defined(_WIN32) || defined(_WIN64)
iResult = send(connfd, str.str().c_str(), str.str().length(), 0);
error(iResult, connfd);
closesocket(connfd);
#else
iResult = write(connfd, str.str().c_str(), str.str().length());
error(iResult, connfd);
close(connfd);
#endif
}
return 0;
}
//Windows客户端代码
#if defined(_WIN32)||defined(_WIN64)
#include <windows.h>
#include <winsock.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
#include <cstring>
#include <iostream>
using namespace std;
int main()
{
WSADATA wsd;
int Ret = 0;
//通知程序,加载socket库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
cout << "socket初始化失败: " << WSAGetLastError() << endl;
return -1;
}
SOCKET CliSoc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKADDR_IN Client; //服务器信息结构
string str;
Client.sin_addr.s_addr = inet_addr(""); //服务器IP地址
Client.sin_port = htons(1234); //端口号
Client.sin_family = AF_INET;
if (connect(CliSoc, (sockaddr*) & Client, sizeof(Client)) == SOCKET_ERROR)
{
cerr << "connect" << endl;
closesocket(CliSoc); //关闭套接字
WSACleanup();
return 1;
}
cout << "连接到主机..." << endl;
char buf[1024] = { 0 };
recv(CliSoc, buf, 1024, 0);
cout << buf << endl;
closesocket(CliSoc); //关闭套接字
WSACleanup();
return 0;
}
测试结果:
Linux下:
Windows下:
上述代码只需修改客户端代码中指定服务器IP地址的部分,在Windows与linux的通信过程中
需要将linux的防火墙关闭,以及确认Windows与linux之间能ping通