C++socket(udp、tcp)常用基础函数笔记

之前用C#做服务器没搞明白于是从笔者比较熟悉的C++开始入手从头学了一遍,整理一下笔记。
资料来源于《网络多人游戏架构与编程》第三章,这本书讲的很明白,比起网上每篇博客都在介绍的原理,这本书更偏向于代码实现。
代码应该没什么问题,笔者已经成功和C#写的客户端连接上了。

头文件及使用

头文件部分
//Windows系统
#include <WinSock2.h>
#include <Ws2tcpip.h> //地址转换库
/*<WinSock2.h>重定义了旧版本的<Winsock>,而<winsock>默认包含在了<Windows.h>文件中
 *所以必须保证在调用<Windows.h>之前#define宏WIN32_LEAN_ADD_MEAN
**/

//POSIX平台
#include <sys/socket.h>
#include <netinet/in.h>	//为了使用部分IPv4特有功能
#include <arpa/inet.h> //地址转换库
#include <netdb.h> //名称解析库
Windows系统启动socket库

在POIX平台中,库在默认情况下就是开启状态,而Winsock2需要显示的启动和关闭,并允许用户指定使用什么版本,使用WSAStartup激活,使用WSACleanup关闭

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
int WSACleanup();
//笔记记录时常用的是2.2版本
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
WSACleanup();
报错

错误报告在各个平台上略有不同,所有平台上大部分函数错误时返回-1,在Windows系统中,可以使用宏SOCKET_ERROR代替-1,因为-1不能显示错误来源,所以Winsock2提供了获取额外错误代码的函数来得到错误原因。

//注意是Windows系统
int WSAGetLastError();

这个函数仅返回当前运行线程的最近错误代码。

类似的,POSIX兼容库也提供了获取错误信息的方法,但是,它们使用C语言标准库中的全局变量errno报告错误代码,所以需要包含<errno.h>文件。

创建SOCKET

SOCKET socket(int af, int type, int protocol);
参数
  1. af参数表示协议簇,指明socket所使用的网络层协议。

    含义
    AF_INEXIPv4
    AF_INET6IPv6
  2. type指明socket发送和接收分组的形式。

    含义
    SOCK_STREAM有序可靠的(适用于TCP)
    SOCK_DGRAM离散的(适用于UDP)
  3. protocol表示发送数据时应使用的协议,值为0时表示用type默认的形式

使用
//调用如下函数创建IPv4 TCP socket
SOCKET tcpsocket = socket(AF_INET, SOCK_STREAM, 0);
//调用如下函数创建IPv4 UDP socket
SOCKET udpsocket = socket(AF_INET, SOCK_DGRAM, 0);
//无论是TCP还是UDP,最后都需要关闭socket连接
int closesocket(SOCKET sock);

当关闭TCPsocket时,应保证所有发送、接收的数据都已经传输确认完毕,所以应停止SOCKET传输,再关闭socket连接。

int shutdown(SOCKET socket, int how);

shutdown()参数how:

含义
SD_SEND停止发送
SD_RECEIVE停止接收
SD_BOTH停止发送和接收

SOCKET地址

每一个网络层数据包都需要一个源地址和一个目的地址,如果数据包封装传输层数据,还需要一个源端口和一个目的端口。为了将地址信息传入和传出socket库,API提供了sockaddr数据类型

struct sockaddr {
    uint16_t sa_family; //常数,指定地址类型,应与创建socket时使用的参数af一致
    char sa_data[14]; //存储真正的地址
};

虽然可以手动填写sa_data,但是你需要各种地址族的内存布局,为了弥补这一点,API为常用地址族提供了帮助地址初始化的专用数据类型。但是因为C语言莫得多态继承,所以传入地址的时候需要手动把专用数据类型转换成sockaddar类型。

sockaddr_in类型:IPv4
struct sockaddr_in{
    short sin_family; //和sockaddr中的sa_family具有相同含义
    uint16_t sin_port; //存储地址中的16位端口部分
    struct in_addr sin_addr; //存储4字节的IPv4地址
    char sin_zero[8]; //不使用,仅为了使sockaddr_in与sockaddr的大小一致,应全为0
};
//不同socket库之间in_addr类型有差异,所以一些平台提供一个结构体以封装这个结构,用于设置不同格式的地址
struct in_addr {
    union {
        struct {
            uint8_t s_b1, s_b2, s_b3, s_b4;	//通过设置此处字段可以以人类可读形式输入地址
        } S_un_b;
        struct {
            uint16_t s_w1, s_w2;
        } S_un_w;
        uint32_t S_addr;
    } S_un;
};

当利用4字节整数设置IP地址或者设置端口号时,很重要的一件事情是考虑TCP/IP协议族和主机有可能在多字节数的字节序上采用不用的标准。为了实现将主机字节序转换为网络字节序的功能,socketAPI提供了htons函数和htonl函数。

uint16_t htons(uint16_t hostshort);
uint32_t htons(uint32_t hostlong);

当然,你可以用如下函数去将网络字节序转换成你能读懂的字节序

uint16_t ntohs(uint16_t networkshort);
uint32_t ntohs(uint32_t networklong);
初始化sockaddr_in

下面的代码展示了如何创建一个IP地址为127.0.0.1,端口为80的socket地址

sockaddr_in myAddr;
memset(myAddr.sin_zero, 0, sizeof(myAddr.sin_zero));
myAddr.sin_family = AF_INET;
myAddr.sin_port = htons(80);
myAddr.sin_addr.S_un.S_un_b.s_b1 = 127;
myAddr.sin_addr.S_un.S_un_b.s_b2 = 0;
myAddr.sin_addr.S_un.S_un_b.s_b3 = 0;
myAddr.sin_addr.S_un.S_un_b.s_b4 = 1;

类型安全

因为socket库最初建立的时候很少考虑类型安全,所以在应用层把基本的socket数据类型和函数封装为自定义的面向对象的结构体是很有帮助的。有助于将socketAPI与你的业务代码中分离出来,以备之后决定将socket库替换成为其它网络库。

用字符串初始化sockaddr

向socket地址添加IP地址和端口有一定发的工作量,特别是地址信息很可能来自程序配置文件或者命令行中的一个字符串,如果是将字符串输入sockaddr,你可以不做处理工作,而是使用如下函数

int inet_pton(int af, const char* src, void* dst); 
/*
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
*/
int inet_pton(int af,const PCTSTR src, void* dst); //#include <WS2tcpip.h>

参数

  1. af即地址族,即AF_INET或AF_INET6。
  2. src应指向空字符(NULL)结尾的字符串,存储英文句号分割的地址。
  3. dst应该指向待赋值的sockaddr和sin_addr字段。

这个函数成功时返回1,源字符串错误返回0,发生其它系统错误返回-1

如下使用inet_pton初始化sockaddr

sockaddr_in myAddr;
myAddr.sin_family = AF_INET;
myAddr.sin_port = htons(80);
inet_pton(AF_INET, "127.0.0.1", &myAddr.sin_addr);

当然这个字符串需要是纯数字的IP地址,如果是域名的话需要将域名解析为地址

int getaddrinfo(
    const char* hostname, //以空字符(NULL)结尾的字符串,存储待查找的域名
    const char* servname, //以空字符(NULL)结尾的字符串,存储端口号或对应服务名称
    const addrinfo* hints, //存储希望收到的结果,可以传入nullptr获取所有匹配的结果
    addrinfo** res //指向新分配的addrinfo结构体链表的头部,每个addrinfo表示来自DNS服务器响应的一部分
);
struct addrinfo {
    int ai_flags;
    int ai_family; //指定从属的地址族(AF_INET)
    int ai_socktype;
    int ai_protocol;
    size_t ai_addrlen; //给出了ai_add指向的sockaddr的大小
    char* ai_canonname; //若ai_flags设置了AI_CANONNAME标记,则存储被解析主机的规范名称
    sockaddr* ai_addr; //存储给定地址族的sockaddr,指向在调用时由参数hostname指定的主机和servname指定的端口
    addrinfo* ai_next; //指向链表中的下一个addrinfo(一个域名可对应多个IPv4或IPv6地址)
};
//因为getaddrinfo分配一个或多个addrinfo结构体,所以在保存了需要的sockaddr后需要释放内存
void freeaddrinfo(addrinfo* ai);	//遍历整个链表释放所有addrinfo节点和相关的缓存

注意:getaddrinfo没有内置的异步操作,会阻塞线程,需要大量的时间(毫秒甚至秒级)。

绑定socket

通知操作系统socket将使用一个特定地址和传输层端口的过程称为绑定。使用bind函数:

int bind(SOCKET sock, const sockaddr* address, int address_len);

bind成功时返回0,失败时返回-1。

通常,你只能将一个socket绑定到一个给定的地址和端口。如果这个地址和端口已经被占用,那么bind返回-1,这种情况下,你可以反复尝试绑定不同端口,知道找到可用端口,地可以给需要绑定的端口赋值为0来自动完成这个操作。

如果一个进程试图使用一个未被绑定的socket发送数据,网络库将自动为这个socket绑定一个可用的端口,因此,手动调用bind函数的唯一原因是指定绑定的地址和端口。

UDP Socket

一旦创建好socket,就可以通过UDP socket发送数据,如果没有绑定,网络模块将在动态端口范围内找一个空闲的端口自动绑定,使用sendto函数发送数据:

int sendto(
    SOCKET sock, //数据包应使用的socket
    const char* buf, //指向待发送数据起始地址的指针,可以是任何能够被转换为cahr*的数据类型
    int len, //待发送数据的大小,最好避免发送大于1300字节的数据包
	int flags, //对控制发送的标志进行按位或运算的结果,大多数游戏代码中该参数为0
    const sockaddr* to, //目标接受者的sockaddr
    int tolen //传入参数to的sockaddr大小,对于IPv4,传入sizeof(sockaddr_in)即可
);

如果操作成功,返回等待发送数据的长度,否则返回-1,若返回0,代表数据已经成功进入发送队列,并不代表数据已经成功发出。

使用recvfrom函数从UDPsocket接收数据:

int recvfrom(SOCKET sock, //查询数据的socket,默认会阻塞直到有数据报到达
	char* buf, //接收数据包的缓冲区
	int len, //指定参数buf可以存储的最大字节数,超出字节将被丢弃
	int flags, //对控制接收的标志进行按位或运算的结果,大多数游戏代码中该参数为0
	sockaddr* from, //该函数会在此写入发送者的地址和端口,不需要初始化
	int* fromlen //存储参数from所指向的sockaddr的大小
);

如果成功执行,返回复制到buf的字节数,如果发生错误则返回-1

TCP Socket

TcpSocket在使用socket和bind函数创建和绑定一个socket之后,需要使用listen函数启动监听:

int listen(
	SOCKET sock,
	int backlog //队列中允许传入的最大连接数,达到最大值后后续连接将会被丢弃
);

成功时返回0,错误时返回-1

接收传入的连接并继续TCP握手过程的时候,调用accept函数:

SOCKET accept(
	SOCKET sock, //接收传入连接的监听socket
	sockaddr* addr, //将会被写入请求连接的远程主机地址,不需要初始化
	int* addrlen //指向addr缓冲区大小的指针,以字节为单位,真正写入地址之后将更新这个参数
);

若accept执行成功,将创建并返回一个可以与远程主机通信的新socket,这个socket被绑定到与监听socket相同的端口号上。默认情况下,如果没有待接收的传入连接,accept函数将阻塞调用线程,直到收到一个传入的连接,或者超时。

客户端应使用connect函数主动与远程服务器握手:

int connect(
	SOCKET sock, //待连接的socket
	const sockaddr* addr, //指向目的远程主机的地址指针
	int addrlen //addr参数所指向地址的长度
);

函数成功时返回0,错误时返回-1,默认情况下,connect会阻塞线程直到连接被接受或者超时。

连接完毕后,使用send函数通过连接的TCP socket发送数据:

int send(
	SOCKET sock,
	const char* buf,
	int len,
	int flags
);

如果send成功,返回发送数据的大小。如果socket的输出缓冲区有一些空余的空间,但不足以容纳整个buf时,这个值可能会比参数len小。如果没有空间,默认情况下,调用线程将被阻塞,直到调用超时或者发送了足够的数据后产生空间。如果发生错误,send函数返回-1。请注意,非零的返回值并不代表数据已经成功发送出去了,只能说明数据被存入队列中等待发送。

调用recv函数从上图一个连接的TCPsocket接收数据

int recv(
	SOCKET sock,
	char* buf,
	int len,
	int flags
);

如果recv调用成功,返回接受的数据大小,这个值小于等于len。当len非零时,如果recv返回0,说明连接的另外一段发送了一个FIN数据包,承诺没有更多需要发送的数据(即可断开)。如果发生错误,recv函数返回-1。默认情况下,recv函数会阻塞调用线程,直到数据流中的下一组数据到达,或者超时。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux双网卡指的是在一台Linux服务器上同时使用两个网卡进行网络连接。通过配置双网卡,可以实现网络负载均衡、网络冗余和网络隔离等功能。 在Linux系统中,可以使用ifconfig命令查看和设置网卡信息。首先,通过ifconfig命令查看当前系统中存在的网卡接口,然后通过配置文件修改网络接口的配置信息,使得两个网卡同时工作。 在C程序中,可以使用socket函数创建UDP套接字,然后使用bind函数将套接字绑定到一个特定的本地IP地址和端口号上。接着,使用recvfrom函数UDP套接字中接收数据报文,并使用sendto函数将数据报文发送到指定的目标IP地址和端口号。 在实际应用中,可以使用两个UDP套接字分别监听两个网卡的IP地址和端口号,以实现双网卡的功能。通过套接字的接收和发送函数,可以在不同的网卡上接收和发送数据报文。 另外,还可以通过设置套接字的选项,如SO_REUSEADDR和SO_BINDTODEVICE等,来实现更精细的控制。 综上所述,Linux双网卡和C语言的socket编程可以结合使用,通过配置双网卡实现网络负载均衡和网络隔离等功能,并使用UDP套接字进行数据的接收和发送。 ### 回答2: Linux双网卡C语言Socket UDP是指在Linux系统下,基于C语言编程,使用SocketUDP协议进行网络通信的双网卡配置。 网卡是计算机连接网络的一种硬件设备,有时我们需要使用多个网卡来实现更高效的网络通信。在Linux系统中,我们可以通过配置双网卡来实现此目的。 使用C语言编程,我们可以使用Socket函数来进行网络通信。Socket是一种能够在不同计算机之间进行通信的API接口,可以用于UDPTCP等网络协议。 UDP是用户数据报协议,它是一种无连接的、不可靠的传输协议,适用于一对一或多对一的通信。UDP在传输数据时效率高,但无法保证数据的可靠性和顺序性。 在双网卡配置中,我们可以使用C语言编写程序,通过SocketUDP协议进行通信。首先,我们需要调用Socket函数创建一个套接字,并使用bind函数将套接字绑定到一个特定的IP地址和端口号上。然后,我们可以使用recvfrom函数接收来自其他计算机的数据,并使用sendto函数将数据发送给其他计算机。 通过配置两个不同的网卡,我们可以利用双网卡进行双向通信。例如,我们可以将一个网卡连接到局域网,另一个网卡连接到互联网,这样就可以实现局域网和互联网之间的数据传输。 总之,Linux双网卡C语言Socket UDP是指在Linux系统下,利用C语言编程,通过Socket函数UDP协议进行双网卡配置和网络通信的技术。这种技术可以实现高效的双向数据传输,并适用于一对一或多对一的通信场景。 ### 回答3: Linux 双网卡 c socket UDP 是指在 Linux 操作系统中,使用 C 编程语言编写的程序,通过 Socket 编程接口,使用 UDP 协议进行网络通信。 双网卡是指计算机系统中同时安装了两块或多块网卡,通过这些网卡可以连接到不同的网络。在使用双网卡的情况下,我们可以在程序中指定使用哪一块网卡进行数据传输,实现数据的接收和发送。在使用双网卡的环境下,程序可以同时监听多个网络上的数据包,并且可以选择性地进行数据的处理。 Socket 是一种网络编程的接口,是一种软件接口,用于支持进程间的网络通信。在 C 语言中,我们使用 Socket 编程接口来创建网络连接,发送和接收数据。通过 Socket 编程接口,我们可以轻松地实现各种网络通信方式,包括 TCPUDP 以及其他协议。 UDP 是用户数据报协议(User Datagram Protocol)的缩写,它是一种无连接的、不可靠的传输协议。与 TCP 不同,UDP 不需要在传输之前建立连接,直接将数据通过 UDP 数据包发送给目标主机。由于 UDP 无连接且不可靠,所以它的传输速度比 TCP 快,但是在传输过程中可能会出现数据丢失或者乱序的问题。在使用 UDP 进行网络通信时,我们需要使用程序来处理这些问题,确保数据的可靠性。 总结来说,Linux 双网卡 C Socket UDP 是指在 Linux 操作系统中,通过 C 编程语言编写的程序,使用 Socket 编程接口,利用 UDP 协议进行网络通信。在双网卡环境下,程序可以指定使用哪一块网卡进行数据传输,并且可以实现数据的接收和发送。同时,程序也需要处理 UDP 协议带来的数据丢失或者乱序等问题,确保数据的可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值