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

一、套接字描述符

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地址的指针,失败返回NULL

in_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的指针,失败返回NULL

int 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结构的指针,失败返回NULL

void 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:一般数据被阶段。
posted @ 2025-03-31 21:31  baobaobashi  阅读(201)  评论(0)    收藏  举报