字节序转换函数
为了解决数据在不同字节序主机之间传播,一般规定发送端总是要把发送的数据转换成大端字节序数据后再发送 这个字节序叫网络字节序。接收端则根据自身字节序来选择转或不转。
网络字节顺序是TCP/IP中规定好的数据表示格式,采用大端排序的方式
BSD socket 提供了封装好转换接口
h - host 主机 主机字节序 to - 2 n - network 网络字节序 s - short短整型 uint16 l - long 长整型 uint32
short 16位 用于转换端口(一个socket最多能提供65535个连接) long 32位 用于转ip的(ip是4字节的)
从主机字节序到网络字节序的转换函数 htons,htonl
从网络字节序到主机字节序的转换函数 ntohs,ntohl
#include <arpa/inet.h>//16位用于转端口uint16_t htons(uint16 hostshort);//主机字节序转网络字节序uint16_t ntohs(uint16 netshort);//网络字节序转主机字节序//32位用于转IPuint32_t htonl(uint16 hostlong);//主机字节序转网络字节序uint32_t ntohl(uint32 netlong);//网络字节序转主机字节序#include <arpa/inet.h>int main(){unsigned short port = 0x0102;unsigned char ip_host[4] = {192, 168, 1, 100};//先将指针变为4字节的类型 再取内容(这样就是直接取unsigned int这么大的内容)//主机字节序转网络字节序unsigned int ipint_host = *(unsigned int *)ip_host; //unsigned int就是longunsigned int ipint_net = htonl(ipint_host);unsigned char *ip_net = (unsigned char *)&ipint_net;printf("%d %d %d %d \n", *ip_net, *(ip_net + 1), *(ip_net + 2), *(ip_net + 3)); //打印网络字节序(大端字节序)的ip//网络字节序转主机字节序ipint_net = *(unsigned int *)ip_net; //unsigned int就是longipint_host = ntohl(ipint_net);// ip_host = (unsigned char *)&ipint_host;//出错 数组名不能做左值 数组名不能act like pointerunsigned char *ip_host_ = (unsigned char *)&ipint_host;// 数组名仅仅是“相当于”指针。而并不是真的是指针,数组名是仅仅是个常量(一个值为数组首元素地址的常量),// 所以不能进行++ 或者–运算。而常量更是无法取地址的,// 而之所以有 &a,事实上这里的a的意义早已经不是当初那个数组名了,它此时代表了整个数组。//数组名做右值时 比如有个数组 in a[] , a == &a == &a[0] 都是指向数组首元素的首地址//但是a+1表示 首地址+sizeof(元素类型) 即就是指向a[1]//&a+1表示 首地址+sizeof(a) 这种一般用于二维数组 如果a是二维数组 就是指向a[1][0]printf("%d %d %d %d \n", *ip_host_, *(ip_host_ + 1), *(ip_host_ + 2), *(ip_host_ + 3)); //打印网络字节序(大端字节序)的ipreturn 0;}
socket地址
结构体 sockaddr socket中将ip,端口等信息封装成了一个数据结构 便于传输
#include<bits/socket.h>struct sockaddr{//专用于ipv4sa_family_t sa_family;//typedef usigned short sa_family_t; 2字节char sa_data[14];};
sa_family_t为地址组类型
协议族(protocol family 也称domain)和对应的地址组如下
![]() |
|---|
注意宏PF*和AF*都定义在bits/socket.h中。且前者和后者有完全相同的值,所以二者通常混用。
不同协议族的地址值sa_data有不同的含义和长度
![]() |
|---|
14字节的sa_data可以存储IPV4的地址,但是无法存另外两个协议族的地址
所以Linux定义了新的通用的socket地址结构,有了足够大的空间并且是内存对齐的
#include<bits/socket.h>#if ULONG_MAX > 0xffffffff# define __ss_aligntype __uint64_t#else# define __ss_aligntype __uint32_t#endif#define _SS_SIZE 128#define _SS_PADSIZE (_SS_SIZE - (2 * sizeof (__ss_aligntype)))struct sockaddr_storage{sa_family_t ss_family; ;//typedef usigned short sa_family_t; 2字节__ss_aligntype __ss_align; //用于内存对齐char __ss_padding[_SS_PADSIZE];};
专用socket地址
很多网络编程都是早于IPV4前诞生的,那时候都是用struct sockaddr结构体,为了兼容以前的内容,现在一般向函数传递socket地址时 强制转换传入(sockaddr*)xxx xxx是sockaddr_in或其他新的地址类型的变量,函数内部根据地址族类型再强制转换为新的地址类型。
![]() |
|---|
//ipv4
#include <netinet/in.h>struct sockaddr_in {sa_family_t sin_family; /* AF_INET __SOCKADDR_COMMON_SIZE*/in_port_t sin_port; /* Port number. 2字节 */struct in_addr sin_addr; /* Internet address. 4字节 *//* Pad to size of `struct sockaddr'. 补到和sockaddr'类型一样大 其实就是再补8字节但是这样port 和 ip作为单独的数据变量 可以.或->直接赋值 比之前的用sockaddr需要指针转换方便很多*/unsigned char sin_zero[sizeof (struct sockaddr) -sizeof (sa_family_t) -//__SOCKADDR_COMMON_SIZEsizeof (in_port_t) -sizeof (struct in_addr)];};typedef unsigned int uint32_t ;typedef unsigned short uint16_t ;typedef uint16_t in_port_t;typedef uint32_t in_addr_t;typedef __SOCKADDR_COMMON_SIZE (sizeof(unsigned short int));struct in_addr {in_addr_t s_addr; /* IPv4 address */};//ipv6struct sockaddr_in6 {sa_family_t sin6_family; /* AF_INET6 */in_port_t sin6_port; /* Transport layer port # */uint32_t sin6_flowinfo; /* IPv6 flow information */struct in6_addr sin6_addr; /* IPv6 address */uint32_t sin6_scope_id; /* IPv6 scope-id */};struct in6_addr {union {uint8_t u6_addr8[16];uint16_t u6_addr16[8];uint32_t u6_addr32[4];} in6_u;#define s6_addr in6_u.u6_addr8#define s6_addr16 in6_u.u6_addr16#define s6_addr32 in6_u.u6_addr32};
IP地址转换函数
一般用点分十进制字符串表示IPV4地址,用十六进制字符串表示IPV6地址,但编程中需要先把它们转化为整数才能使用。
下面3个函数用于 点分十进制字符串IPV4地址 和 用网络字节序整数表示的IPV4地址(二进制形式 4字节)之间的转换。
都可用man查看这些接口
#include<arpa/inet.h>in_addar_t inet_addr(const char* cp);//点分十进制字符串 转为 网络字节序整型数int inet_aton(const char* cp,struct in_addr* inp);//点分十进制字符串 转为 网络字节序整型数 inp为传出参数 为网络字节序的4字节整型数//成功返回1 失败返回0typedef uint32_t in_addr_t;struct in_addr {in_addr_t s_addr; /* IPv4 address */};char* inet_ntoa(struct in_addr in);//网络字节序整数 转为 点分十进制字符串
上面3个接口比较旧了 现在一般用下面两个
int inet_pton(int af,const char *src,void *dst); //同时适用于ipv4和ipv6地址//p表示点分十进制的ip字符串 转为 n表示网络字节序的ip整数形式(二进制形式 4字节)//af为要转换地址的地址族v4orv6 AF_INET or AF_INET6//src就为点分十进制ip字符串//dst为传出的网络字节序ip二进制形式 ipv4地址 传入参数为unsigned int*类型//返回1成功 0传入src参数非法 -1失败const char* inet_ntop(int af,const void* src,char* dst,socklen_t size);//同时适用于ipv4和ipv6地址//网络字节序的ip整数形式(二进制形式 4字节)转为点分十进制的ip字符串//af为要转换地址的地址族v4orv6 AF_INET or AF_INET6//src为传入的网络字节序ip二进制形式 ipv4地址 传入参数为unsigned int*类型//dst为传出的点分十进制的ip字符串//size 传入实参(dst)字符串数组的size//成功返回dst(字符串)的首地址 失败返回 nullchar ip[] = “192.168.1.1”;//点分十进制ip字符串unsigned int ip_net = 0;inet_pton(AF_INET,ip,&ip_net);//验证转换从整数二进制ip地址成功unsigned char *p = (unsigned char *)&ip_net;printf(“%d %d %d %d”,*p,*(p+1),*(p+2),*(p+3));//大端 p低地址char ip_[16]="";//ipv4字符串最长为 3+3+3+3(4个地址不超255 长度最长为3) + 3(3个.) + 字符串结束符 = 16const char*ip__ = inet_ntop(AF_INET,&ip_net,ip_,sizeof(ip_));printf(“%s”,*ip__);返回值 printf(“%s”,*ip_);传出值 是一样的
UDP和TCP区别
UDP 用户数据协议报 面向无连接,不仅仅只支持1对1的单播,也可以多播和广播 面向数据报,发送固定大小数据报,通信双方不需要建立连接 发送方不关心数据是否到达 接收方不会备份收到的数据 是不可靠的通信。 UDP没有拥塞控制,以恒定速度发送数据,就算网络不好也保持恒定速度发数据 会导致丢数据。 高实时性场景下使用
TCP 传输控制协议 面向连接 可靠的通信,基于字节流,仅支持1对1单播。
![]() |
|---|
TCP通信流程
![]() |
|---|
服务器(接受到连接求才开始工作 在此之前一直监听)
创建一个用于监听的套接字(文件描述符 fd)
将监听套接字fd与本地(服务器)的IP和端口绑定(客户端要连接的就是这个ip与端口)
启动开始监听等待客户端向我(服务器)发送连接请求,在接受到连接请求前一直阻塞监听。
经过三次握手四次挥手,连接创建成功会得到一个和客户端通信的套接字(accept得到的fd 通过这个fd可以向客户端通信 注意之前的那个套接字fd只用于监听而不用于通信)
开始通信 读写
通信结束 断开连接
客户端(主动连接服务器)
创建一个用于通信的套接字fd
连接服务器connect,需要指定连接的服务器的ip和端口
连接成功开始通信 读写
通信结束 断开连接
socket函数
#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>//包含了这个上面两个可以省略int socket(int domain,int type,int protocol);//创建一个套接字//domain 协议族 AF_INET AF_INET6 AF_UNIX(AF_LOCAL)//type 协议类型 SOCK_STREAM 双向 可靠 基于连接的字节流(流式协议)//SOCK_DGRAM 无连接 不可靠 数据报协议//protocol 具体的协议 就是一个协议类型可能包含多个这种类型的协议 这个参数就是用于指定是要用这种协议类型中的哪个协议。//SOCK_STREAM下protocol传0 使用TCP//SOCK_DGRAM下protocol传0 使用UDP//返回值 创建成功返回这个套接字文件的描述符fd(这个fd代表的并非真实文件 而是内核中的一个读写缓冲区) 失败返回-1int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);//将fd和本地的ip和端口进行绑定//sockfd socket函数得到的文件描述符//addr socket结构体包含我们要绑定的ip和端口 传入//addrlen addr大小//成功返回0 失败返回-1int listen(int sockfd,int backlog);// cat /proc/sys/net/core/somaxconn 一个端口的最大监听队列的长度 一个端口最多可以监听多少个可能连接的客户端 4096//将传入的socket变为被动的socket(socket函数创建的都是主动型的) 这个socket将被accept函数用于监听是否有对应的连接请求 (listen不阻塞 accept阻塞)//sockfd socket函数得到的文件描述符//backlog sockfd对应的等待连接队列的最大长度//如果客户端请求时,服务器的等待连接队列满了,客户端的connect将会得到一个ECONNERFUSED错误。//这个最大队列长度一般设定为 未连接(未完成三次握手队列)+已连接队列(以完成三次握手队列) 的最大长度 未连接队列最大长度 cat /proc/sys/net/ipv4/tcp_max_syn_backlog//成功返回0 失败返回-1//int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen)//接受客户端连接 默认阻塞等待客户端连接//sockfd socket函数得到的文件描述符(此fd 是之前用于监听的socket)//服务器需要这个fd 去获得连接的客户端的信息//addr 传出 连接成功后客户端socket地址信息(客户端的ip和端口)//addrlen addr的大小//成功返回 用于通信的服务器socket fd 失败返回-1int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen)//客户端用于连接服务器的函数//sockfd 客户端用于通信的fd (客户端不需要bind 只需要一个文件描述符 系统会随机为客户端进程分配一个端口)//addr 客户端要连接的服务器socket地址//addrlen addr的大小//成功返回0 失败返回-1//发送接收就是用的之前的 文件读写函数ssize_t write(int fd,const void *buf,size_t count);ssize_t read(int fd,void *buf,size_t count);
/* tcp服务器socket()->bind()->listen()->accept()->{recv send}->recv->close*/#include <arpa/inet.h>#include <stdio.h>#include <unistd.h> //read write#include <string.h> // strlen#include <stdlib.h>int main(){//1 创建用于监听的套接字int sock_listen = socket(AF_INET, SOCK_STREAM, 0); //ipv4 AF_INET SOCK_STREAM, 0tcp协议//socket fd 并非真实文件 而是内核中的一块读写缓冲区if (sock_listen == -1){perror("socket:");exit(-1);}//2 绑定 socket 绑定 服务器ip和端口struct sockaddr_in sa_sever;sa_sever.sin_family = PF_INET; //ipv4协议族 用AF_INET也一样inet_pton(AF_INET, "192.168.91.0", sa_sever.sin_addr.s_addr); //网络字节序整型ip// sa_sever.sin_addr.s_addr = 0;//INADDR_ANY = 0代表 0.0.0.0代表任意地址 其实是代表服务器上的多个网卡的ip地址 通过这多个网卡地址 都能访问服务器sa_sever.sin_port = htons(9999); //网络字节序整型端口 主机字节序转网络字节序int ret = bind(sock_listen, (struct sockaddr *)&sa_sever, sizeof(sa_sever));if (ret == -1){perror("bind:");exit(-1);}//3 监听ret = listen(sock_listen, 128);if (ret == -1){perror("listen:");exit(-1);}//4 等待客户端连接 阻塞struct sockaddr_in client_addr; //用于接收客户端的socket地址信息socklen_t len = (socklen_t)sizeof(client_addr);int sock_com = accept(sock_listen, (struct sockaddr *)&client_addr, &len); //返回用于和客户端通信的 sockfdif (sock_com == -1){perror("accept:");exit(-1);}//输出连接上的客户端信息//accept得到的客户端信息char client_ip[16];inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)); //网络字节序的整型 需要先转成主机字节序的字符串unsigned short client_port = ntohs(client_addr.sin_port); //网络字节序整型 转为主机字节序整型printf("client ip:%s port:%d\n", client_ip, client_port); //客户端因为不需要bind所以客户端的os会随机为客户端分配一个端口 通信双方不需要端口相同 端口号只是标识了是那个ip主机的哪个进程在通信//5 通信//获取客户端数据char recvbuf[1024] = {0};int len_read = read(sock_com, recvbuf, sizeof(recvbuf)); //无数据 阻塞if (len_read == -1){perror("read:");exit(-1);}else if (len_read == 0){//客户端断开连接 写端关闭 则读端读返回0printf("client closed\n");}else if (len_read > 0)printf("recv from client %s\n", recvbuf);//向客户端发送数据char *s = "hello i am server";write(sock_com, *s, strlen(s));//6 关闭文件sock描述符close(sock_com);close(sock_listen);return 0;}/* tcp客户端socket()->connect()->{recv send}->close*/#include <arpa/inet.h>#include <stdio.h>#include <unistd.h> //read write#include <string.h> // strlen#include <stdlib.h>int main(){//1 创建用于通信的套接字int sock_com = socket(AF_INET, SOCK_STREAM, 0); //ipv4 AF_INET SOCK_STREAM, 0tcp协议//socket fd 并非真实文件 而是内核中的一块读写缓冲区if (sock_com == -1){perror("socket:");exit(-1);}//2 连接服务器//服务器socket地址struct sockaddr_in sa_sever;sa_sever.sin_family = PF_INET; //ipv4协议族 用AF_INET也一样inet_pton(AF_INET, "192.168.91.0", sa_sever.sin_addr.s_addr); //网络字节序整型ipsa_sever.sin_port = htons(9999); //网络字节序整型端口 主机字节序转网络字节序int ret = connect(sock_com, (struct sockaddr *)&sa_sever, sizeof(sa_sever));if (ret == -1){perror("connect:");exit(-1);}//3 通信//向服务器端发送数据char *s = "hello i am client";write(sock_com, *s, strlen(s));//读服务器数据char recvbuf[1024] = {0};int len = read(sock_com, recvbuf, sizeof(recvbuf)); //无数据 阻塞if (len == -1){perror("read:");exit(-1);}else if (len == 0){//客户端断开连接 写端关闭 则读端读返回0printf("server closed\n");}else if (len > 0)printf("recv from server %s\n", recvbuf);//4 关闭文件sock描述符close(sock_com);return 0;}





