Linux网络编程:Socket套接字

一、socket地址API
1、主机字节序和网络字节序

小端字节序(主机字节序)是指一个整数的高位字节存储在内存的高地址处
大端字节序(网络字节序)是指一个整数的高位字节存储在内存的低地址处

判断机器字节序

#include <stdio.h>

void byteorder(){
    union 
    {
        short value;
        char union_bytes[sizeof(short)];
    }test;

    test.value = 0x0102;
    if ((test.union_bytes[0] == 1)&& (test.union_bytes[1] == 2)) {
        printf("big endian\n");
    }else if((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1)){
        printf("little endian\n");
    }else{
        printf("unknown\n");
    }
}

主机字节序和网络字节序之间的转换

#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
功能:长整型的主机字节序转化为网络字节序
    
unsigned short int htons(unsigned short int hostshort);
功能:短整型的主机字节序转化为网络字节序
    
unsigned long int ntohl(unsigned long int netlong);
功能:长整型的网络字节序转化为主机字节序
    
unsigned short int ntohs(unsigned short int netshort);
功能:短整型的网络字节序转化为主机字节序
2、通用socket地址
#include <bits/socket.h>
struct sockaddr{
    sa_family_t sa_family; // 地址族类型
    char sa_data[14]; // 存放socket地址值
}
// 协议族(protocol family 也称domain)
协议族    地址族  描述                地址值含义和长度
PF_UNIX  AF_UNIX  UNIX本地域协议族    文件的路径名,长度可到达108字节
PF_INET  AF_INET  TCP/IPv4协议族     16bit端口号和32bitV IPv4地址,共6个字节
PF_INET6 AF_INET6 TCP/IPv6协议族     16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共有26字节

// 为解决sa_data无法容纳多数协议族的地址值,定义新的通用socker地址结构体
struct sockaddr_storage{
    sa_family_t sa_family;
    unsigned long int __ss_align;
    char __ss_padding[128-sizeof(__ss_align)];
}
3、专用socket地址
#include <sys/un.h>
struct sockaddr_un{
    sa_family_t sin_family; // AF_UNIX
    char sun_path[108]; // 文件路径名
}
struct sockaddr_in{
	sa_family_t sin_family; // 地址族
    u_int16_t sin_port;     // 端口号,要用网络字节序表示
    struct in_addr sin_addr; // IPv4地址结构体
};
struct in_addr{
    u_int32_t s_addr; // IPv4,要用网络字节序表示
};
struct sockaddr_in6{
    sa_family_t sin6_family; // 地址族:AF_INET6
    u_int16_t sin6_port;     // 端口号,要用网络字节序表示
    u_int32_t sin6_flowinfo; // 流信息,应设置为0
    struct in6_addr sin6_addr; // IPv6地址结构体
    u_int32_t sin6_scope_id; // scope ID,尚处于实验阶段
};
struct in6_addr{
    unsigned char sa_addr[16]; // IPv6地址,要用网络字节序表示
};

注意:所有专用socket地址类型在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可)
4、IP地址转换函数
#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
功能:将用点分十进制字符串表示的IPv4的地址转化为用网络字节序整数表示的IPv4.
返回值:失败:INADDR_NONE

int inet_aton(const char* cp, struct in_addr* ip);
功能:同inet_addr同样的功能。将转化的结果存储于参数inp指向的地址结构中
返回值:成功 1
       失败 0

char *inet_ntoa(struct in_addr in);
功能:将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址(函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的

int inet_pton(int af, const char* src. void* dst);
功能:将用字符串表示的IP地址src转换成网络字节序整数表示的IP地址,并将转化结果存储于dst指向的内存中
参数:af 指定地址族 AF_INET 或者AF_INET6
     src IP地址字符串
     dst 转化结果存储于dst指向的内存中
返回值 成功 1
      失败 0 设置errno

const char* inet_ntop(int af, void *src, char *dst, socklen_t cnt);
功能:af,src,dst同上
    cnt指定目标存储单元的大小 
    	#define INET_ADDRSTRLEN  16
        #define INET6_ADDRSTRLEN 46
返回值:
    成功 目标存储单元的地址
    失败 NULL并设置errno
二、创建socket socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:
参数:
    domin:告诉系统使用底层协议族
    	IPv4 PF_INET 
    	IPv6 PF_INET6
        本地协议族 PF_UNIX
     type 指定服务类型
    	SOCK_STREAM TCP
    	SOCK_DGRAM  UDP
        SOCK_NONBLOCK 设置为非阻塞的
    	SOCK_CLOEXEC 用fork调用创建子进程时在子进程中关闭该socket
     protocol 通常设置为0,表示默认协议
返回值:
    成功 返回一个socket文件描述符
    失败 -1并设置errno
三、绑定socket bind
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
功能:将sockfd与my_addr进行绑定
参数:sockfd:socket创建出来的文件描述符
     my_addr:服务器的地址
     addrlen:my_addr的大小   sizeof(my_addr)
返回值:
    成功: 0
    失败:-1并设置errno
      EACCES:被绑定的地址是受保护的地址,仅超级用户能够访问
      EADDRINUSE:被绑定的地址正在使用中
 
/*
	注意:服务器中,就需要为sockfd与my_addr进行绑定,因为只有绑定后客户端才能知道如何连接它
         客户端不需要绑定,而是采用匿名方式,也就是操作系统自动分配socket地址
*/
四、监听socket listen
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:创建监听队列以存放待处理的客户连接
参数:sockfd:指定被监听的socket
     backlog:内核监听队列的最大长度,连接数超过backlog,客户端收到ECONNREFUSED错误信息
返回值:
    成功 0
    失败 -1并设置errno
五、接受连接 accept
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:从listen监听队列中接受一个了连接
参数:sockfd:执行过里listen系统调用的文件描述符
     addr:客户端socket地址
     addrlen:addr的长度
返回值:
    成功:与客户端通信的文件描述符,读写都是通过这个描述符来进行的
    失败:-1并设置errno
六、发起连接 connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
功能:客户端发起连接服务器
参数:
    sockfd:客户端的sock文件描述符
    serv_addr:服务器地址
    addrlen:serv_addr的长度
返回值:
    成功  0
    失败 -1并设置errno
    	ECONNREFUSED:目标端口不存在,连接被拒绝
    	ETIMEDOUT:连接超时
七、关闭连接 close
#include<unistd.h>
int close(int fd);
功能:关闭fd,并不是立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用计数为0时,才真正关闭连接
    
#include<sys/socket.h>
int shutdown(int sockfd, int howto);
功能:立即终止连接
参数:
    howto
      SHUT_RD:关闭读
      SHUT_WR:关闭写
      SHUT_RDWR:关闭读写
返回值:
    成功  0
    失败 -1并设置errno
八、数据读写
1、TCP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:读取sockfd上的数据
参数:
    sockfd:文件描述
    buf:指定缓冲区的位置
    len:指定缓冲区的大小
    flags:0
返回值:
    成功:实际读取的数据的长度
    失败:
    	0 对方已经关闭连接
    	错误 -1并设置errno

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:往sockfd上写入数据
参数:
    sockfd:文件描述符
    buf:指定缓冲区的位置
    len:缓冲区的大小
    flags:0
返回值:
    成功 返回实际写入数据的长度
    失败 -1并设置errno

flags可以取以下选项的一个或几个的逻辑或

选项名含义sendrecv
MSG_CONFIRM指示数据链路层协议持续监听对方的回应,直到得到答复。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socketYN
MSG_DONTROUTE不查看路由表,直接将数据发送给本地局域网络内的主机。这表示发送者确切地知道目标主机就在本地网络上YN
MSG_DONTWAIT对socket的此次操作将是非阻塞的YY
MSG_MORE告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可房子TCP发送过多小的报文段,从而提高传输效率YN
MSG_WAITALL读操作仅在读取到指定数量的字节才返回NY
MSG_PEEK窥探读缓存中的数据,此次读操作不会导致这些数据被清除NY
MSG_OOB发送或接收紧急数据YY
MSG_NOSIGNAL往读端的管道或者socket连接中写数据时不引发SIGPIPE信号YN
2、UDP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const  void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
参数:
    sockfd 读写数据文件描述符
    buf:读写缓冲区的位置
    len:缓冲区的大小
    flags:0
    src_addr:获取发送端的socket地址
    dest_addr:指定接收端的socket地址
    addrlen:地址长度
 
// 注意:recvform/sendto也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接受端的地址
   
3、通用数据读写函数
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);
参数:
    sockfd 读写数据文件描述符
    msg msghdr结构体类型指针
    flags 0
    
struct msghdr
{
	void msg_name;            // socket地址
    socklen_t msg_namelen;    // socket地址的长度
    struct iovec* msg_iov;    // 分散的内存块
    int msg_iovlen;           // 分散内存块的数量
    void *msg_control;        // 指向辅助数据的起始位置
    socklen_t msg_controllen; // 辅助数据的大小
    int msg_flags;            // 复制函数中的flags参数,并在调用过程中更新
};

struct iovec
{
	void *iov_base; // 内存起始地址
    size_t iov_len; // 这块内存的长度
};
iovec结构体封装了一块内存的起始位置和长度,msg_iovlen指定这样的分散的iovec结构对象有多少个
msg_control和msg_controllen 用于辅助数据的传送
msg_flags 无须设定,它会复制函数的第三个参数flags,recvmsg还会在调用结束强,将某些更新后的标志设置到msg_flags中
九、带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
功能:判断sockfd是否处于带外标记,下一个被读取到的数据是否是带外数据
参数:sockfd 文件描述符
返回值:
    10 不是

用sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。

十、地址信息函数
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* address, socklen_t *address_len);
功能:获取sockfd对应的本端socket地址
参数:
    sockfd 文件描述符
    address 存储地址的内存
    address_len 地址长度
返回值:
    0 成功
    -1 失败

int getpeername(int sockfd, struct sockaddr* address, socklen_t *address_len);
功能:
    获取sockfd对应的远端socket地址
参数:同getsockname

十一、socket选项
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void* option_value,
               socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value,
               socklen_t option_len);
功能:读取和设置socket文件描述符属性
参数:sockfd 指定被操作目标socket
     level 参数指定要操作的那个协议的选项
    	SOL_SOCKET 通用socket选项,与协议无关
    	IPPROTO_IP IPv4选项
    	IPPROTO_IPV6 IPv6选项
    	IPPROTO_TCP TCP选项
     option_name:指定选项的名字,常用以下几个
    	SO_REUSEADDR:重用socket地址 int
    	SO_RCVBUF:TCP接收缓冲区的大小 int
    	SO_SNDBUF:TCP发送缓冲区的大小 int
    	SO_RCVLOWAT:TCP接收缓冲区的低水位标记,默认1字节 int
    	SO_SNDLOWAT:TCP发送缓冲区的低水位标记,默认1字节 int
    	SO_LINGER:如下 linger
     option_value:被操作选项的值
     option_len:被操作选项的长度
返回值:
    0 成功
    -1 失败并设置errno
    
SO_LINGER:
// SO_LINGER用于控制close系统调用在关闭TCP连接时的行为。
// 通常,使用close系统调用来关闭socket时,close将立即返回,TCP模块负责把该socket对应TCP发送缓冲区中残留的数据发送给对方。
#include <sys/socket.h>
struct linger{
    int l_onoff;   // 开启(非0)还是关闭(0)该选项
    int l_linger;  // 滞留时间
};
- l_onoff = 0,此时SO_LINGER选项不起作用,close用默认行为来关闭socket
- l_onoff != 0,l_linger = 0,此时close系统立即返回,TCP模块丢弃被关闭的socket对应的TCP发送缓冲区中残留的数据,同时给对方发送一个复位报文段,,提供了异常终止一个连接的方法
- l_onoff != 0,l_linger大于0,此时close的行为取决于两个条件:1.TCP缓冲区中是否残留都数据,2. socket是阻塞的,还是非阻塞的
    - 阻塞:close将等待一段长为l_linger的时间,如果没有发送往并得到对方确认,close系统调用返回-1并设置errno为EWOULDBLOCK
    - 非阻塞:close将立即返回,根据返回值和errno判断数据是否发送完毕

十二、网络信息API
1、gethostbyname和gethostbyaddr
//
#include <netdb.h>
struct hostent* gethostbyname(const char *name);
功能:根据主机名称获取主机的完整信息
参数:
    name:目的主机的主机名
返回值:hostent结构体类型的指针
    
struct hostent* gethostbyaddr(const void *addr, size_t len, int type);
功能:根据IP地址获取主机的完整信息
参数:
    addr:目标主机的IP地址
    len:addr所只IP地址的长度
    type:IP地址的类型,   AF_INET(IPv4) AF_INEF69(IPv6)
返回值:hostent结构体类型的指针

struct hostent{
    char* h_name;       // 主机名
    char** h_aliases;   // 主机别名列表,可能有多个
    int  h_addrtype;    // 地址类型
    int h_length:       // 地址长度
    char** h_addr_list; // 按网络字节序列出的主机IP地址列表
};
2、getservbyname和getservbyaddr
//
#include <netdb.h>
struct servent* getservbyname(const* name, const char* proto);
功能:根据名称获取某个服务的完整信息
参数:
    name:目标服务的名字
    proto:指定服务类型,“tcp”获取流服务 “udp”获取数据报服务  NULL获取所有类型的服务
返回值:
    servent结构体类型的指针

struct servent* getservbyport(int port, const char* proto);
功能:根据端口号获取某个服务的完整信息
参数:
    port:目标服务的端口号
    proto:指定服务类型,“tcp”获取流服务 “udp”获取数据报服务  NULL获取所有类型的服务
返回值:
    servent结构体类型的指针
    
struct servent{
    char* s_name;      // 服务名称
    char** s_aliases;  // 服务的别名列表,可能有多个
    int s_port;        // 端口号
    char* s_proto;     // 服务类型,通常是tcp或者udp
}

注意:以上4个函数都是不可重入的,即非线程安全的,不过netdb.h头文件给出它们的可重入版本,正如Linux下所有其他函数的可重入的命名规则那样。这些函数的函数名是在原函数名尾部加上_r(re-entrant)。

3、getaddrinfo
#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
功能:既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)
    
参数:
    hostname:接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)
    service:接收服务名,也可以接收字符串表示的十进制端口号
    hints:应用程序给getaddrinfo的一个提示,对getaddrinfo的输出进行更精确的控制。
    	NULL,允许getaddrinfo反馈任何结果
    result:参数指向一个链表,该链表用于存储getaddrinfo反馈的结果
返回值:
    成功 0
    失败 返回错误码

struct addrinfo{
    int ai_flags;              // 下面说明
    int ai_family;             // 地址族
    int ai_socktype;           // 服务类型,SOCK_STREAM或SOCK_DGRAM
    int ai_protocol;           // 下面说明
    socklen_t ai_addrlen;      // socket地址ai_addr的长度
    char* ai_canonname;        // 主机的别名
    struct sockaddr* ai_addr;  // 指向socket地址4
    struct addrinfo* ai_next;  // 指向下一个sockinfo结构的对象
};
// ai_protocol:具体的网络协议,其含义和socket系统调用的第三个参数相同,通常设置为0
// 使用hints参数的时候,可以设置ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段必须设置为NULL

// result需要调用下面函数来释放这块内存
void freeaddrinfo(struct addrinfo* res);

// 使用案例:
struct addrinfo hints;
struct addrinfo *res;
bzero(&hints, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
getaddinfo("ernest-laptop","daytime", &hists, &res);
...
freeaddrinfo(res);

ai_flags成员可以取以下的标志的按位或

选项含义
AI_PASSIVEhists参数中设置,表示调用者是否会将取得的socket地址用于被动打开,服务器通常设置它,表示接受任何本地socket地址上的服务请求。客户端程序不能设置它
AI_CANONNANE在hists参数中设置,告诉getaddrinfo函数返回主机的别名
AI_NUMERICHOST在hists参数中设置,表示hostnam必须是用字符串表示的IP地址,从而避免了DNS查询
AI_NUMERICSERV在hists参数中设置,强制service参数使用十进制端口号的字符串形式,而不能是服务名
AI_V4MAPPED在hists参数中设置,如果ai_family被设置为AF_INET6,那么当没有满足条件的IPv6地址被找到时,将IPv4地址映射为IPv6地址
AI_ALL必须和AI_V4MAPPED同时使用,否则将被忽略。表示同时返回符合条件的IPv6地址以及由IPv4地址映射得到的IPv6地址
AI_ADDRCONGIG仅当至少配置有一个IPv4地址(除了回路地址)时,才返回IPv4地址信息;同样,仅当至少配置有一个IPv6地址(除了回路地址)时,才返回IPv6地址信息。它和AI_V4MAPPED时互斥的
4、getnameinfo
int getnameinfo(const struct sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);
功能:通过socket地址同时获得以字符串表示的主机名和服务名
参数:sockaddr:socket地址
    host:主机名
    hostlen:主机名的长度
    serv:服务号
    servlen:服务号的长度
    flags:控制getnameinfo的行为
返回值:
    成功 0 
    失败 返回错误号
// 将错误码转换成易读的字符串形式
const char* gai_strerror(int error);

flags参数

选项含义
NI_NAMEREQD如果通过socket地址不能获取主机名,则返回一个错误
NI_DGRAM返回数据服务。大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务。但端口512~514是例外。比如TCP的514端口提供的是shell登录服务,而UDP的514端口提供的是syslog服务
NI_NUMERICHOST返回字符串表示的IP地址,而不是主机名
NI_NUMERICSERV返回字符串表示的十进制端口号,而不是服务名
NI_NOFQDN仅返回主机域名的第一部分。比如对主机名nebula.testing.com,getnameinfo只将nebula写入host缓存中

错误码表

选项含义
EAI_AGAIN调用临时失败,提示应用程序过后再试
EAI_BADFLAGS非法的ai_flags值
EAI_FAIL名称解析失败
EAI_FAMILY不支持的ai_family参数
EAI_MEMORY内存分配失败
EAI_NONAME非法的主机名或服务名
EAI_OVERFLOW用户提供的缓冲区溢出。仅发生在getnameinfo调用中
EAI_SERVICE没有支持的服务,比如用数据报服务类型来查找ssh服务。因为ssh服务只能使用流服务
EAI_SOCKTYPE不支持的服务类型。如果hints.ai_socktype和hists.ai_protocol不一致,比如前者指定SOCK_DGRAM,而后者使用的是IPROTO_TCP,则会触发这类错误
EAI_SYSSTEM系统错误,错误值存储在errno中
  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodeKwang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值