网络编程——socket套接字入门

news/2025/4/2 10:39:52/文章来源:https://www.cnblogs.com/baobaobashi/p/18803039

一、套接字描述符

1、创建一个套接字

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//返回值:成功返回文件描述符,失败返回-1
  • domain:指定通信地址族,通常以AF_开头(Address Family)
    • AF_INET:IPv4
    • AF_INET6:IPv6
    • AF_UNIX:UNIX域
    • AF_PACKET:原始套接字
  • type:确定套接字类型
    • SOCK_STREAM:TCP
    • SOCK_DGRAM:UDP
    • SOCK_RAW:原始套接字
  • protocol:指定协议类型,通常为0,表示使用默认协议,
    • TCP:IPPROTO_TCP
    • UDP:IPPROTO_UDP
    • ICMP:IPPROTO_ICMP
    • IGMP:IPPROTO_IGMP
    • IPV4:IPPROTO_IP
    • IPV6:IPPROTO_IPV6

2、禁止一个套接字

#include <sys/socket.h>
int shutdown(int sockfd, int how);
//返回值:成功返回0,失败返回-1
  • sockfd:套接字描述符
  • how:指定关闭方式
    • SHUT_RD:关闭读端
    • SHUT_WR:关闭写端
    • SHUT_RDWR:关闭读写端

3、关闭一个套接字

#include<unistd.h>
int close(int fd);
//返回值:成功返回0,失败返回-1
  • fd:套接字描述符

二、寻址

1、字节序转换函数

有些处理器使用小端字节序,有些处理器使用大端字节序。但是网络协议可能使用的字节序与处理器不同(如TCP/IP使用大端字节序),因此需要将字节序和网络字节序之间进行转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • htonl:将主机字节序转换为网络字节序
  • htons:将主机字节序转换为网络字节序
  • ntohl:将网络字节序转换为主机字节序
  • ntohs:将网络字节序转换为主机字节序

h表示主机字节序,n表示网络字节序,l表示长整型,s表示短整型。

2、地址格式

地址格式与通信域相关。为了使不同的格式地址都能传入套接字函数,地址会被强制转换成一个通用的结构体sockaddr。

struct sockaddr 
{sa_family_t sa_family; /* address family, AF_xxx */char sa_data[];     
};
  • sa_family:地址族
  • sa_data:协议地址

套接字可以自由的添加额外的成员并定义sa_data的大小。

a、ipv4格式

在IPV4因特网域(AF_INET)中,sockaddr_in结构体被定义如下:

struct in_addr
{uint32_t s_addr;    //IPV4地址
};
struct sockaddr_in
{sa_family_t         sin_family; /* 地址族: AF_INET */in_port_t           sin_port;       /* 端口号,网络字节序 */struct in_addr      sin_addr;    /* IP地址,网络字节序 */
}
  • in_port_t被定义为uint16_t
  • in_addr_t被定义为uint32_t

b、ipv6格式

IPv6因特网域(AF_INET6)中,sockaddr_in6结构体被定义如下:

struct in6_addr
{uint8_t s6_addr[16];    //IPV6地址
};
struct sockaddr_in6
{sa_family_t     sin6_family; /* 地址族: AF_INET6 */in_port_t       sin6_port;       /* 端口号,网络字节序 */uint32_t        sin6_flowinfo;   /* IPv6流信息 */struct in6_addr sin6_addr;    /* IPv6地址,网络字节序 */uint32_t        sin6_scope_id;  /* IPv6作用域ID */
};

c、Linux中ipv4格式

在Linux中,sockaddr_in结构体被定义为:

struct sockaddr_in
{sa_family_t         sin_family; /* 地址族: AF_INET */in_port_t           sin_port;       /* 端口号,网络字节序 */struct in_addr      sin_addr;    /* IP地址,网络字节序 */unsigned char       sin_zero[8]; /* 8个字节,填充0 */
}

注意:尽管sockadd_in和sockaddr_in6结构差距比较大,但是它们都被强制转换成sockaddr结构体,因此可以传入套接字函数。

3、地址转换函数

注意:这两个函数仅支持IPv4地址

  • inet_addr函数将点分十进制IP地址转换为二进制IP地址。

  • iner_ntoa函数将二进制IP地址转换为点分十进制IP地址。

#include <arpa/inet.h>
const char *inet_ntoa(struct in_addr in);
//返回值:成功返回指向点分十进制IP地址的指针,失败返回NULLin_addr_t inet_addr(const char *cp);
//返回值:成功返回指向in_addr结构的指针,失败返回INADDR_NONE

注意:下面这两个函数同时支持IPv4和IPv6地址

  • inet_pton函数将点分十进制IP地址转换为二进制IP地址。

  • inet_ntop函数将二进制IP地址转换为点分十进制IP地址。

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *restrict addr, char *restrict str, socklen_t size);
//返回值:成功返回指向str的指针,失败返回NULLint inet_pton(int af, const char *restrict str, void *restrict  addr);
//返回值:成功返回1,失败返回0
  • af:地址族
  • src:指向点分十进制IP地址的指针
  • addr:指向二进制IP地址的指针

4、INADDR_ANY

INADDR_ANY:表示任意地址。

  • 它表示服务器将接受来自任何网络接口的请求
  • 当服务器需要监听多个网络接口时,可以使用INADDR_ANY来绑定所有网络接口,而不需要指定具体的IP地址。
#define INADDR_ANY ((in_addr_t) 0x00000000)

使用方法:

struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
//此处可以不进行将主机字节序转换成网络字节序,因为是0
//address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(1234);

5、地址查询

a、getaddrinfo函数

用于获取主机信息。

#include <netdb.h>
struct hostent *gethostent(void);
//返回值:成功返回指向hostent结构的指针,失败返回NULLvoid sethostent(int stayopen);
void endhostent(void);

hostent结构体定义如下:

struct hostent
{char *h_name;            /* 主机名 */char **h_aliases;        /* 主机别名列表 */int h_addrtype;        /* 地址类型 */int h_length;            /* 地址长度 */char **h_addr_list;    /* IP地址列表 */...
}

b、gethostbyname(已过时)

通过主机名(如www.example.com)获取对应的IPv4地址。

struct hostent *gethostbyname(const char *name);
//返回值:成功返回指向hostent结构的指针,
//        失败返回NULL,可通过h_errno变量获取错误码。
  • name:主机名
  • addr:IP地址
  • len:IP地址长度
  • type:地址类型

注意:

  • 仅支持IPv4。
  • 非线程安全(不可重入)。
  • 已逐渐被更现代的getaddrinfo替代

c、gethostbyaddr(已过时)

通过IPv4地址获取对应的主机名(反向DNS查询)。

struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
//返回值:成功返回指向hostent结构的指针,
//        失败返回NULL,可通过h_errno变量获取错误码。
  • addr:指向IPv4地址的指针(需转换为struct in_addr格式)。

  • len:地址长度(IPv4为4)。

  • type:地址类型(AF_INET表示IPv4)。

  • 现在由getnameinfo函数替代

d、getnameinfo(推荐)

将一个地址转换成 一个主机名和服务名。

#include <netdb.h>
#include <sys/socket.h>int getnameinfo(const struct sockaddr *restrict addr, socklen_t addrlen,char *restrict host, socklen_t hostlen,char *restrict serv, socklen_t servlen, int flags);
//返回值:成功返回0,失败返回错误码
  • addr:指向sockaddr结构的指针。
  • addrlen:sockaddr结构的长度。
  • host:指向主机名的指针。
  • hostlen:主机名缓冲区的大小。
  • serv:指向服务名的指针。
  • servlen:服务名缓冲区的大小。
  • flags:控制函数行为的标志。

host非空,则指向一个长度为hostlen的缓冲区用于存放返回的主机名

serv非空,则指向一个长度为servlen的缓冲区用于存放返回的服务名

flags参数可以控制函数的行为,例如:

  • NI_DGRAM:指定协议类型为UDP,而不是TCP。
  • NI_NOFQDN:不返回完整的域名,只返回主机名。
  • NI_NUMERICHOST:返回数字形式的IP地址,而不是主机名。
  • NI_NAMEREQD:如果无法解析主机名,则返回错误。
  • NI_NUMERICSERV:返回数字形式的端口号,而不是服务名。

f、getaddrinfo(推荐)

将主机名和服务名转换成套接字地址。

#include <netdb.h>
#include <sys/socket.h>
int getaddrinfo(const char *rest host, const char *restrict serv,const struct addrinfo *restrict hints,struct addrinfo **restrict res);
//返回值:成功返回0,失败返回错误码void freeaddrinfo(struct addrinfo *ai); 
//释放addrinfo结构体
  • host:主机名或IP地址。
  • serv:服务名或端口号。
  • hints:指向addrinfo结构的指针,用于指定查询的约束条件。
  • res:指向addrinfo结构体的指针,用于存放查询结果。
    //返回值:成功返回0,失败返回错误码

hints 选择符合特定条件的地址信息,例如:

  • AI_ADDRESS_FAMILY:指定地址族(AF_INET或AF_INET6)。
  • AI_ALL:查找IPV4和IPV6地址。(仅用于AI_V4MAPPED)
  • AI_CANONNAME:返回规范主机名。
  • AI_NUMERICHOST:仅返回数字形式的IP地址。
  • AI_NUMERICSERV:仅返回数字形式的端口号。
  • AI_PASSIVE:用于服务器端,返回通配符地址(INADDR_ANY)。
  • AI_V4MAPPED:如果找不到IPv6地址,返回映射到IPV6格式的IPV4地址。

注意:

  • 需要提供主机名服务名,或者两个都提供,如果只提供一个另外一个必须是空指针
    主机名可以是一个域名,也可以是一个IP地址。

  • getaddrinfo返回一个addrinfo结构体的链表,可以使用freeaddrinfo函数释放这个链表,其中可以包含一个或多个addrinfo结构,用于表示不同的地址。

6、将套接字和地址关联

bind函数将套接字与地址关联起来。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:成功返回0,失败返回 -1

注意:

  • 指定的地址必须有效,不能指定一个其他机器上的地址。
  • 地址必须与地址组所支持的格式匹配。
  • 地址中端口号不能小于1024,除非进程具有超级用户权限。
  • 一般只能将一个套接字端点与一个地址关联。

如果指定IP地址为INADDR_ANY,则表示服务器将接受来自任何网络接口的请求。

使用getsockname来发现绑定到套接字上的地址。

#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//返回值:成功返回0,失败返回 -1
  • addr:指向sockaddr结构的指针,用于存放返回的地址。
  • alenp:指向socklen_t类型的指针,用于存放返回的地址长度。

三、建立连接

1、connect函数

客户端使用connect函数与服务器建立连接。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:成功返回0,失败返回 -1
  • sockfd:套接字描述符。
  • addr:指向sockaddr结构的指针,用于指定服务器的地址。
    如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。

连接服务器可能会出现失败,服务器必须是开启的,并且正在运行,并且服务器的等待连接队列要有足够的空间。因此,应用程序需要处理connect返回的错误。

//可能出现错误的connect
for(numsec = 1; numsec <= MAXSLEEP; numsec <<= 1) 
{if(connect(fd, servaddr, servlen) == 0){return 0;}if(numsec <= MAXSLEEP / 2){sleep(numsec);} 
}

注意:使用指数退避算法,在连接失败后会休眠一段时间,然后重试。每次重试的间隔时间以指数级增加,直到达到最大延迟(通常为2分钟)。这种方法在Linux和Solaris上有效,但在FreeBSD和Mac OS X上存在问题,因为基于BSD的系统在首次连接失败后,继续使用同一个套接字描述符会失败

我们可以每次都创建一个新的套接字描述符来连接服务器,这样就可以避免这个问题。

for(numsec = 1; numsec <= MAXSLEEP; numsec <<= 1)
{if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 1){return -1;}if(connect(fd, servaddr, servlen) == 0){return 0;}close(fd);if(numsec <= MAXSLEEP / 2){sleep(numsec);}
}

2、listen函数

服务器使用listen函数宣告它愿意接受连接请求。

#include <sys/socket.h>
int listen(int sockfd, int backlog);
//返回值:成功返回0,失败返回 -1
  • sockfd:套接字描述符。

  • backlog等待连接队列的最大长度
    参数backlog指定了等待连接队列的最大长度,即服务器最多可以有多少个连接请求在等待被处理。
    如果等待连接队列已满,新的连接请求将被拒绝。

    实际长度由系统决定,但是上限由<sys/socket.h> 中的SOMAXCONN指定。

    Solaris系统中,会忽视SOMAXCONN,具体的最大值取决于每个协议的实现。对于TCP,最大值通常为128。

3、accept函数

一旦服务器调用了listen,所用的套接字就能接收到连接请求。使用accept函数获得连接请求并建立连接。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);//返回值:成功返回新的套接字描述符,失败返回 -1

accept返回的是新的套接字描述符,这个新套接字描述符和原始套接字描述符(sockfd)具有相同的类型和地址族。

原始套接字描述符(sockfd)没有关联到这个连接,而是继续保持可用状态并接受连接

如果服务器调用accept,并且当前没有连接请求,那么服务器将阻塞,直到有连接请求到达。服务器可以使用poll或select来等待一个请求的到来,在这种情况下,一个带有等待连接的套接字描述符将变为可读

四、数据传输

1、发送数据

a、send函数

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//返回值:成功返回发送的字节数,失败返回 -1
  • sockfd:套接字描述符。
  • buf:指向缓冲区的指针,用于存放接收到的数据或发送的数据。
  • len:缓冲区的大小。
  • flags:控制函数行为的标志。

send与write类似,使用send时套接字必须已经连接。参数buf和len与write中一致。
不同的是,send可以指定一些额外的标志(flags)

flags

  • MSG_CONFIRM:提供链路层反馈以保持地址映射有效。
  • MSG_DONTROUTE:勿将数据包路由出本地网络。
  • MSG_DONTWAIT:允许非阻塞操作。
  • MSG_EOF:发送数据后关闭套接字发送端。
  • MSG_EOR:如果协议支持,标记记录结束。

即使send成功返回,也不代表连接的另一端接收到了数据。但是此时数据已经被无错误的发送到了网络驱动程序上。

b、sendto函数

#include <sys/socket.h> 
sisize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *destaddr, socklen_t destlen);
//返回值:成功返回发送的字节数,失败返回 -1
  • sockfd:套接字描述符。
  • buf:指向缓冲区的指针,用于存放接收到的数据或发送的数据。
  • len:缓冲区的大小。
  • flags:控制函数行为的标志。
  • destaddr:指向sockaddr结构的指针,用于指定目标地址。
  • destlen:目标地址的长度。

sendto函数与send函数类似,但是sendto函数可以用于无连接的套接字,例如UDP套接字。sendto函数需要指定目标地址,而send函数不需要。

c、sendmsg函数

#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//返回值:成功返回发送的字节数,失败返回 -1
  • sockfd:套接字描述符。
  • msg:指向msghdr结构的指针,用于指定要发送的数据。
  • flags:控制函数行为的标志。

sendmsg函数可以一次发送多个缓冲区。sendmsg函数的参数msg是一个msghdr结构,它包含了要发送的数据和目标地址。

msghdr结构的定义如下:

struct msghdr {void         *msg_name;       /* 指向sockaddr结构的指针,用于指定目标地址 */socklen_t     msg_namelen;    /* 目标地址的长度 */struct iovec *msg_iov;        /* 指向iovec结构的指针,用于指定要发送的数据 */int           msg_iovlen;     /* iovec结构的数量 */void         *msg_control;    /* 指向cmsghdr结构的指针,用于指定控制信息 */socklen_t     msg_controllen; /* 控制信息的长度 */int           msg_flags;      /* 控制标志 */
};
  • msg_name:指向sockaddr结构的指针,用于指定目标地址。
  • msg_namelen:目标地址的长度。
  • msg_iov:指向iovec结构的指针,用于指定要发送的数据。
  • msg_iovlen:iovec结构的数量。

2、接收数据

a、recv函数

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//返回值:成功返回接收到的字节数,失败返回 -1
  • sockfd:套接字描述符。
  • buf:指向缓冲区的指针,用于存放接收到的数据。
  • len:缓冲区的大小。

flags:控制函数行为的标志。

  • MSG_CMSG_CLOEXEC:为UNIX域套接字接受的文件描述符设置关闭标志。

  • MSG_DONTWAIT:允许非阻塞操作。

  • MSG_ERRQUEUE:从套接字的错误队列中接收数据。

  • MSG_OOB:如果协议同意,接收带外数据。

  • MSG_PEEK:返回数据包内容而不真正取走数据包。

  • MSG_TRUNC:返回数据包的实际长度,即使它比缓冲区大。

  • MSG_WAITALL:等待所有数据到达。

    当指定MSG_PEEK标志时,可以查看下一个要读取的数据但不真正取走它。当再次调用read或者其中一个recv函数时,会返回刚才查看的数据

    对于SOCK_STREAM套接字,接受的数据可能比预期的少,MSG_WAITALL标志可以用来等待所有数据到达。

b、recvfrom函数

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,struct sockaddr *restrict addr, socklen_t *restrict addrlen);
//返回值:成功: 返回接收到的字节数,
//        若无可用数据或连接已关闭: 返回0,
//        失败: 返回 -1
  • sockfd:套接字描述符。
  • buf:指向缓冲区的指针,用于存放接收到的数据。
  • len:缓冲区的大小。
  • flags:控制函数行为的标志。

如果addr非空,它将包含数据发送者套接字端点地址

因为可以获得发送者的地址,recvform通常被用于无连接的套接字。否则recvfrom == recv。

c、recvmsg函数

recvmsg函数可以 一次接收多个缓冲区,还可以接收辅助信息

#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//返回值:成功: 返回接收到的字节数,
//        若无可用数据或连接已关闭: 返回0,
//        失败: 返回 -1
  • sockfd:套接字描述符。
  • msg:指向msghdr结构的指针,用于指定要接收的数据。
  • flags:控制函数行为的标志。

常见的flags有:

  • MSG_CTRUNC:如果控制信息太长,则截断。
  • MSG_EOR:如果协议支持,标记记录结束。
  • MSG_ERRQUEUE:从套接字的错误队列中接收数据作为辅助信息。
  • MSG_OOB:接收带外数据。
  • MSG_TRUNC:一般数据被阶段。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/908838.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图论(连通分量)

AT_abc284_c [ABC284C] Count Connected Components 题目描述 頂点に $ 1 $ から $ N $ の番号が、辺に $ 1 $ から $ M $ の番号がついた $ N $ 頂点 $ M $ 辺の単純無向グラフが与えられます。辺 $ i $ は頂点 $ u_i $ と頂点 $ v_i $ を結んでいます。 グラフに含まれる連結…

业务系统基础框架-Winform版-角色

角色列表,可刷新,可展开,可折叠编辑角色为角色权限为角色分配菜单查看拥有此角色的账号

3.31 学习记录

实现了使用springboot从文件中读取数据显示在前端

记一次GC导致线上服务超时问题

1、现象2024-12-28 23点左右,线上其他服务请求 content-cache 出现批量超时。content-cache-03 机器内存使用率如下:机器配置:4核8G这里因为JVM参数设置为:-Xms4g -Xmx4g -XX:MaxNewSize=1g所以达到42%时,内存的使用率已经达到了3.3G。 2、数据查看GC日志如下: (1)CMS老…

No.1 可视化大屏--vite+vue3项目环境搭建

一、DataV-Vue3 1.1 安装 官网:https://datav-vue3.netlify.app/Guide/Guide.htmlnpm install @kjgl77/datav-vue3 一、vite3构建Vue3项目 1.1 什么是Vite 1.2创建vite3项目 第一步:新建一个项目的文件夹第二步:输入cmd,回车 第三步: npm init vite 第四步:输入项目名称…

SvelteKit 最新中文文档教程(16)—— Service workers

前言 Svelte,一个语法简洁、入门容易,面向未来的前端框架。 从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1:Svelte 以其独特的编译时优化机制著称,具有轻量级、高性能、易上手等特性,非常适合构…

OLLAMA 自定义大模型角色

在Ollama中通过deepseek-r1生成特定角色的模板(如教案设计),核心是通过Modelfile定义模型的系统提示(SYSTEM)和对话模板(TEMPLATE)。以下是具体步骤和示例: 一、Modelfile 基本结构与关键指令FROM指令指定基础模型,这里你可以使用ollama本地下载的模型,也可以去Huggi…

WebSocket调试神器对决:Apipost凭何碾压Apifox?

你以为所有API工具都能玩转WebSocket? 当你的APP需要实时股票行情推送,当你的游戏要处理千人同屏交互,当你的IM系统必须保障消息零延迟——传统HTTP协议的"一问一答"模式瞬间破功。此刻WebSocket协议才是真正的救世主,这个全双工通信协议能让客户端与服务器建立&…

一年前的无心之举,一年后我想要将其做的更好——公众号开通。

大家好,答应的事情要做到。 我是晚秋,我在这里,这是我的公众号。 一年前我想把学过的技术,解决得问题都记录下来,帮助更多的人。 日复一日,也放弃过。 但是到今日,忽然看到自己的无心之举帮助了很多刚进入技术这一行的人。 他们迷茫,他们困顿,正如当初的我一样。 谢谢…

用户头像呼吸光环+鼠标悬停旋转放大

用户头像呼吸光环+鼠标悬停旋转放大在 子比主题后台 – 自定义代码 – 自定 CSS 样式代码 里面添加下面代码: /*【用户头像呼吸光环+鼠标悬停旋转放大】开始 */ .avatar{border-radius: 50%; animation: light 4s ease-in-out infinite; transition: 0.5s;}.avatar:hover{tran…

开源守护,智护童年——幼儿园未成年行为与安全智能监控系统

在孩子成长的每一步,安全始终是第一位的。幼儿园作为孩子们探索世界的起点,其安全管理的重要性不言而喻。然而,哭闹、打闹、意外跌倒,甚至外部隐患如陌生人逗留、内部管理疏漏等问题,常常让传统人工监控捉襟见肘。家长们也迫切希望了解孩子在园的点滴,渴望一份安心与信任…