假如有一个16位的整数,它占了2字节,有两种存储方法
小端字节序:将低序字节存储在起始地址
大端字节序:将高序字节存储在起始地址
这两种方法都有系统在用,所以网络通信需要转化字节序
我们把某个系统上使用的字节序称为 主机字节序
把网络协议使用的字节序称为 网络字节序
网络字节序都是采用大端字节序
而主机字节序却没有标准而言
原型
uint16_t htons(uint16_t h16bitval); // 主机转网络,短型
uint32_t htonl(uint32_t h32bitval); // 主机转网络,长型
uint16_t ntohs(uint16_t n16bitval);// 网络转主机,短型
uint32_t ntohl(uint32_t n32bitval);// 网络转主机,长型
h表示主机,n表示网络,s表示short,l 表示long
字节操纵函数有两组
一是
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
其中bzero是将目标字节串中指定大小的字节数置为0,参数dest是指向首地址的指针,参数nbytes是需要设置的字节数。
bcopy函数用来复制内存的
参数src 为源内存块指针,dest 为目标内存块指针,n 为要复制的内存的前 n 个字节长度
bcmp的功能是比较ptr1和ptr2的前n个字节是否相等
如果ptr1=ptr2或n=0则返回零,否则返回非零值。bcmp不检查NULL。
二是
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src,size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
memset是将目标字节串的指定数目(len)置为值 c
memcpy,和bcopy作用和用法一致,注意参数顺序不一样
memcmp 功能和bcmp一致
我们在表示ipv4的IP地址时习惯使用点分十进制来表示,用的是字符串的形式
但是在实际使用中需要使用IP地址的二进制形式
所以需要一个函数来进行互相转换
原型
int inet_aton(const char *strptr, struct in_addr *addrptr)
/*将字符串地址转化为二进制地址 成功返回1,否则返回0*/
char *inet_ntoa(struct in_addr inaddr);
/*将二进制地址转换为字符串地址, 返回一个点分十进制表示的字符串地址 */
其中的struct in_addr是地址结构内的成员,接下来会讲解
但是以上两个函数,只支持ipv4的地址
接下来两函数,支持ipv4和ipv6
int inet_pton(int family, const char *strptr, void *addrptr)
/*将字符串地址转化为二进制地址 成功返回1,输入无效返回0,错误返回-1*/
char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
/*将二进制地址转换为字符串地址, 返回一个点分十进制表示的字符串地址 */
两个函数的参数family 可以是 AF_INET(表示IPv4),AF_INET6 (表示IPV6)
第一个函数的参数strptr是需要转换的字符串的指针,addrptr用于存放结果
第二个函数的addrptr是需要转换的二进制,strptr是存放结果字符串的指针,len是str的长度,太小会报错
很多套接字函数需要一个指向套接字地址结构的指针作为参数,每个协议族都有自己的地址结构体
但是最终都会强制转换为通用的socket地址结构来传参给函数
通用套接字 sockaddr 类型定义:
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
}
ipv4对应的是sockaddr_in类型定义:
注意这里的sin_addr 是一个结构体,
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr {
uint32_t s_addr;
};
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
in_port_t sin_port; /* 2 bytes port*/
struct in_addr sin_addr; /* 4 bytes IPv4 address*/
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[8]; /* 8 bytes unused padding data, always set be zero */
};
ipv6对应的sockaddr_in6类型定义:
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
}
struct sockaddr_in6 {
sa_family_t sin6_family; /*2B*/
in_port_t sin6_port; /*2B*/
uint32_t sin6_flowinfo; /*4B*/
struct in6_addr sin6_addr; /*16B*/
uint32_t sin6_scope_id; /*4B*/
};
Unix域对应的sockaddr_un类型定义:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
在使用地址结构前,我们一般先将所有字节都置0,使用czero或memset函数,然后在赋值
接下来我们将在使用时再讨论
socket函数的作用,就相当于我们要读文件时要先open
socket会创建一个socket描述符(和文件描述符一样),后续将使用它进行连接等操作
对于服务器来说,这个描述符是用于监听连接的,实际连接传输的描述符在accept处介绍
原型
int socket(int family, int type, int prttocol);
/* 成功返回描述符,出错返回-1
注意以下出参数并不是任意的搭配都是有效的
一般SOCK_STREAM是TCP/SCTP
SOCK_DGRAM是UDP
两个都可以和AF_INET、AF_INET6搭配
参数family,表示协议族,取值有
取值 | 含义 |
---|---|
AF_INET | IPV4 |
AF_INET6 | IPV6 |
AF_LOCAL | UNIX域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 秘钥套接字 |
参数type指明套接字类型
取值 | 含义 |
---|---|
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
参数protocol是某个协议类型常值,通常设为0,让其默认选择。
应用实例
进行tcp连接时
socket(AF_INET,SOCK_STREAM,0);
将一个本地协议地址赋予一个套接字,就是绑定ip地址和端口的
通常服务器在启动的时候都会绑定一个众所周知的地址(如
ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。
通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了
原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 成功返回0,失败返回-1 */
参数sockfd:是socket描述字,bind()函数就是将给这个描述字绑定地址和端口
参数addrlen:对应的是地址的长度
参数addr:地址结构指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核
应用实例
struct socket_in sockaddr_in;
int port = 12345;
memset(&sockaddr_in,0,sizeof(sockaddr_in));
sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(port);
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY 代表可以运行任何ip连接
bind(socket_fd, (struct sockaddr *)&sockaddr_in,sizeof(sockaddr_in));
socket创建的描述符,默认是一个主动类型的(就是主动调用connect去连接别人的,是一个客户端),调用listen后转为主动的,并开始监听socket描述符,等待用户连接
原型
int listen(int sockfd, int backlog);
/* 成功返回0,失败返回-1 */
参数sockfd,是socket描述符
参数backlog 是最大连接个数
最大连接数说明
TCP建立连接是要进行三次握手,但是完成三次握手后,服务器需要维护这种状态:
半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态为:SYN_RCVD)。
全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;
在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小,
在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小
SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。
Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为
128
服务器在调用accept函数后,会阻塞监听socket,等待客户端连接,并返回一个全新的描述符fd,代表与客户端的tcp连接
原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* 成功返回一个用于连接的描述符,失败返回-1 */
参数sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
参数*addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等,结构体与bind中的一致
参数addrlen: 返回客户端协议地址的长度
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
tcp客户端在创建socket后,使用connect来连接服务器
这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 成功返回0,失败返回-1 */
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址结构,这里面包含有服务器的IP地址和端口等信息,和bind的一致
addrlen: socket地址的长度
实例
struct socket_in sockaddr_in;
int port = 12345;
char *ip = “192.168.1.1”
memset(&sockaddr_in,0,sizeof(sockaddr_in));
sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(port);
inet_aton(ip, &sockaddr_in,sin_addr);
connect(socket_fd, (struct sockaddr *)sockaddr_in, sizeof(sockaddr_in));
以上的函数已足够服务器和客户端建立tcp连接
接下来我们介绍用于通信的函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t
*addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
其中read 和 write用法与文件io中的一致,用法如下
UNIX环境编程(c语言)–文件I/O-文件共享
其他函数也不再一一介绍,用法大同小异,详细用法可以man手册查看
使用close关闭通信,就和文件io中的用法一样,详情看上面那个链接文件io的文章
如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候我们需要数据发送出去并到达对方之后才能关闭
socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如
果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了