【网络编程】套接字 | 网络字节序 | IP地址转换函数 | sockaddr数据结构 | 网络套接字函数
我们在系统编程里面学了进程间通信有:
- 无名管道
- 命名管道
- mmap
- 文件信号
- 消息队列
- 共享内存
但是只能用于 本机的进程间通信,现在我们学习一种不同的主机间进程通信方法: socket
1.套接字概念
Socket本身有“插座”的意思,成对出现,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
套接字的内核实现较为复杂,不宜在学习初期深入学习。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的==一对一关系==。
套接字通信原理如下图所示:
**在网络通信中,套接字一定是成对出现的。**一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区
2. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。(大端小端在文章下方扩展)
TCP/IP协议规定,==网络数据流应采用大端字节序==,即低地址高字节。例如网络基础的的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //32位无符号整型的主机字节顺序到网络字节顺序的转换(小端->>大端)
uint16_t htons(uint16_t hostshort);//16位无符号短整型的主机字节顺序到网络字节顺序的转换 (小端->>大端)
uint32_t ntohl(uint32_t netlong); //32位无符号整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)
uint16_t ntohs(uint16_t netshort); //16位无符号短整型的网络字节顺序到主机字节顺序的转换 (大端->>小端)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
示例:
3.IP地址转换函数
3.1inet_pton函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能: 将点分十进制串 转成32位网络大端的数据("192.168.1.2" ==>192.168.1.2 )
参数:
af :
AF_INET IPV4
AF_INET6 IPV6
src: 点分十进制串的首地址
或者使用宏:
INADDR_ANY 通配地址,值为0
dst : 32位网络数据的地址 -->(void*)&dst
成功返回1
失败返回-1
3.2 inet_ntop函数
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
功能: 将32位大端的网络数据转成点分十进制串
参数:
af : AF_INET
src : 32位大端的网络数 地址
dst : 存储点分十进制串 地址
size : 存储点分制串数组的大小
例:“192.168.1.1” -》255.255.255.255 15+'\0'最低16个
或者使用宏:
INET_ADDRSTRLEN 宏的值 16
INADDR_ANY 通配地址,值为0
返回值: 存储点分制串数组首地址
代码示例:
#include<stdio.h>
#include<arpa/inet.h>
int main()
{
char buf[]="192.168.1.4";
unsigned int num = 0;
inet_pton(AF_INET,buf,&num);//一般用在发送转网络
unsigned char* p = (unsigned char*)#
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));
char ip[16] = " ";
printf("%s\n",inet_ntop(AF_INET,&num,ip,16));//用于接收打印
return 0;
}
运行结果:
xcc@ubuntu:~/test $ ./a.out
192 168 1 4
192.168.1.4
4.sockaddr数据结构
网络通信要解决三大问题:协议,IP,端口,我们把它们放到一个结构体里,假如我要发送数据我需要数据内容,ip,端口;方便点用结构体封装起来
strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
可参看 man 7 ip
ipv4套接字结构体
struct sockaddr_in {
sa_family_t sin_family; //协议 AF_INET
in_port_t sin_port; //端口
struct in_addr sin_addr; //ip地址
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */ 4字节
};
ipv6套接字结构体
struct sockaddr_in6 {
unsigned short int sin6_family; /* AF_INET6 */
__be16 sin6_port; /* Transport layer port # */
__be32 sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
__u32 sin6_scope_id; /* scope id (new in RFC2553) */
};
这里会产生一个矛盾:
我这里有ipv4和ipv6,到底是用ipv4 还是ipv6呢?总不能各定义一个接口,分别处理它们吧;那要是有一个int类型和char类型,我们怎么直接使用它们呢?可以用万能类型:void* 成为通用结构体,不管是什么协议类型都可以调用
当然这里没有void,我们可以把struct sockaddr*
看成void*,目的为了接口统一
通用套接字结构体
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
它们之间的协议都一样,不管ipv4还是ipv6,取它们的地址都能找到
Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock-addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下,例如:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */
5.网络套接字函数
5.1 socket模型创建流程图
使用NetAssist.exe工具实现指令通信
5.2 socket函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
tcp传输控制协议特点: 出错重传 每次发送数据对方都会回ACK, 可靠 tcp是抽象打电话的模型建立练级 使用连接 关闭连接
5.3 connect函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
5.4 TCP-client:
include <unistd.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SIZE 1024
int main()
{
//创建套接字
int sock_fd = -1;
char buf[SIZE]=" ";
struct sockaddr_in addr;
sock_fd = socket(AF_INET,SOCK_STREAM,0);
if( -1 == sock_fd)
{
perror("socket");
return 1;
}
//连接服务器
addr.sin_family = AF_INET; //ipv4
addr.sin_port = htons(8888);//端口号转网络字节序
inet_pton(AF_INET,"192.168.0.119",&addr.sin_addr.s_addr);//ip字串
connect(sock_fd,(struct sockaddr *)&addr,sizeof(addr));
//读写数据
while(1)
{
//从终端读
int n = read(STDIN_FILENO,buf,sizeof(buf));//fgets()
write(sock_fd,buf,n);//发送数据给服务器
//从服务器读取并返回个数,标准输出到屏幕
n = read(sock_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,n);
printf("\n");
}
//关闭套接字文件描述符
close(sock_fd);
return 0;
}
首先打开我的NetAssist.exe
,代码是连接服务器操作,在协议类型中选择==TCP Server==,然后查看本地IP地址,并设置端口号,随意设置,代码内容也需要同步ip和端口号(需联网)下载地址
效果展示:
- 点赞
- 收藏
- 关注作者
评论(0)