linux_网络通信-套接字通信socket-网络字节序-IP地址转换函数-inet_pton函数-htonl函数-htons函数-ntohl函数-ntohs函数

接上一篇:linux_进程锁与文件锁-pthread_mutexattr_init函数-pthread_mutexattr_setpshared函数

  今天开始分享网络通信了,主要是就是socket套接字通信,本篇先分享一些预备知识,有网络字节序以及一些IP地址转换函数,话不多说,开始上菜:

此博主在CSDN发布的文章目录:我的CSDN目录,作为博主在CSDN上发布的文章类型导读

1.套接字概念

  Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
  既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
  套接字的内核实现较为复杂,不宜在学习初期深入学习。
  在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。
  “IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
  套接字通信原理如下图所示:
在这里插入图片描述
  在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
  TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。

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);
	uint16_t htons(uint16_t hostshort);
	uint32_t ntohl(uint32_t netlong);
	uint16_t ntohs(uint16_t netshort);

  h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
  int a = 7845;//0x1EA5(两个字节)
  大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法,存储方式:1EA5。
  小端字节序:低位字节在前,高位字节在后,存储方式:A51E。(计算机存储方式)

2.1.htonl函数

函数作用:
  将无符号整数hostlong从主机字节顺序转换为网络字节顺序。
头文件:
  #include <arpa/inet.h>
函数原型:
  int32_t htonl(uint32_t hostlong);
函数参数:
  hostlong:需要转换的无符号主机字节
返回值:
  返回一个网络字节顺序的值。

2.2.htons函数

函数作用:
  从主机字节转换无符号短整数hostshort顺序到网络字节顺序。
头文件:
  #include <arpa/inet.h>
函数原型:
  uint16_t htons(uint16_t hostshort);
函数参数:
  hostshort:需要转换的无符号短整数主机字节
返回值:
  返回一个网络字节顺序的值。

2.3.ntohl函数

函数作用:
  从网络字节顺序转换无符号整数netlong到主机字节顺序。
头文件:
  #include <arpa/inet.h>
函数原型:
  uint32_t ntohl(uint32_t netlong);
函数参数:
  netlong:需要转换的无符号整数网络字节顺序的值。
返回值:
  返回主机字节顺序的值。(32位)
注意:

2.4.ntohs函数

函数作用:
  从网络字节转换无符号短整数netshort顺序到主机字节顺序。
头文件:
  #include <arpa/inet.h>
函数原型:
  uint16_t ntohs(uint16_t netshort);
函数参数:
  netshort:需要转换的无符号短整数网络字节顺序的值。
返回值:
  返回主机字节顺序的值。(16位)

3.IP地址转换函数

早期:
  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  int inet_aton(const char *cp, struct in_addr *inp);
  in_addr_t inet_addr(const char *cp);
  char *inet_ntoa(struct in_addr in);
①只能处理IPv4的ip地址
②是不可重入函数
③注意参数是struct in_addr

现在:
  #include <arpa/inet.h>
  int inet_pton(int af, const char *src, void *dst);
  const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

①支持IPv4和IPv6
②可重入函数
③其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr。

  因此函数接口是void *addrptr。

3.1.inet_pton函数

函数作用:
  将点分文本的IP地址转换为二进制网络字节序”的IP地址。
头文件:
  #include <arpa/inet.h>
函数原型:
  int inet_pton(int af, const char *src, void *dst);
函数参数:
af:地址簇
  取值:AF_INET --表示IPV4
  取值:AF_INET6 --表示IPV6
src:源地址
dst:转换后的地址
返回值:
  返回1:成功;
  返回0:输入不是有效表达式;
  返回-1:失败。
例如:
   struct sockaddr_in serv_addr;
   char buf[BUFSIZ];
   bzero(&serv_addr, sizeof(serv_addr));
   serv_addr.sin_family = AF_INET;
   inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
  serv_addr.sin_port = htons(SERV_PORT);

3.2.inet_ntop函数

函数作用:
  将数值格式转化为点分十进制的ip地址格式。
头文件:
  #include <arpa/inet.h>
函数原型:
  const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
函数参数:
af:地址簇
  取值:AF_INET --表示IPV4
  取值:AF_INET6 --表示IPV6
src:源地址
dst:转换后的地址

返回值:
  若成功则为指向结构的指针,若出错则为NULL,将errno置为EAFNOSUPPORT。
注意:

3.3.bzero函数

函数作用:
  将指定内存清0。
头文件:
  #include <strings.h>
函数原型:
  void bzero(void *s, size_t n);
函数参数:
  s:需要清空内存的首地址;
  n:s的大小
返回值:
无。
例如:
  struct sockaddr_in serv_addr;
  bzero(&serv_addr, sizeof(serv_addr));

4.sockaddr数据结构

  strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。

sockaddr数据结构:
在这里插入图片描述

struct sockaddr {
	sa_family_t sa_family; 		/* address family, AF_xxx */
	char sa_data[14];			/* 14 bytes of protocol address */
};

  使用 sudo grep -r “struct sockaddr_in {” /usr 命令可查看到struct sockaddr_in结构体的定义。一般其默认的存储位置:/usr/include/linux/in.h 文件中。

struct sockaddr_in {
	__kernel_sa_family_t sin_family; 			/* Address family */  	地址结构类型
	__be16 sin_port;					 		/* Port number */		端口号
	struct in_addr sin_addr;					/* Internet address */	IP地址
	/* Pad to size of `struct sockaddr'. */
	unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
	sizeof(unsigned short int) - sizeof(struct in_addr)];
};

struct in_addr {						/* Internet address. */
	__be32 s_addr;
};

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) */
};

struct in6_addr {
	union {
		__u8 u6_addr8[16];
		__be16 u6_addr16[8];
		__be32 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
};

#define UNIX_PATH_MAX 108
	struct sockaddr_un {
	__kernel_sa_family_t sun_family; 	/* AF_UNIX */
	char sun_path[UNIX_PATH_MAX]; 	/* pathname */
};

  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 */

  以上就是本次的分享了,希望对大家有所帮助,欢迎关注博主一起学习更多的新知识!

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,下面是一个简单的 C 语言示例代码,实现了基于 TCP 协议的网络通信程序,要求输入用户名,判断用户名正确后,向客户端发送连接正确的字符串,并在服务器显示客户端的 IP 地址和端口。 服务端代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define MAXLINE 1024 // 最大读取字节数 int main(int argc, char *argv[]) { int listenfd, connfd; struct sockaddr_in servaddr, cliaddr; char buffer[MAXLINE]; char *username = "test"; // 定义用户名 socklen_t len; time_t ticks; // 创建 socket listenfd = socket(AF_INET, SOCK_STREAM, 0); // 初始化服务器地址结构体 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(8080); // 绑定地址和端口号 bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // 监听 listen(listenfd, 10); printf("Listening on port 8080...\n"); while (1) { len = sizeof(cliaddr); // 接受客户端连接 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len); printf("Connection from %s, port %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, buffer, sizeof(buffer)), ntohs(cliaddr.sin_port)); // 读取客户端输入的用户名 bzero(buffer, MAXLINE); read(connfd, buffer, MAXLINE); // 判断用户名是否正确 if (strcmp(buffer, username) == 0) { // 向客户端发送连接正确的字符串 write(connfd, "Connection established.\n", 24); } else { // 向客户端发送连接错误的字符串 write(connfd, "Connection refused.\n", 20); // 关闭连接 close(connfd); continue; } // 获取当前时间 ticks = time(NULL); // 向客户端发送当前时间 snprintf(buffer, sizeof(buffer), "%.24s\r\n", ctime(&ticks)); write(connfd, buffer, strlen(buffer)); // 关闭连接 close(connfd); } return 0; } ``` 客户端代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define MAXLINE 1024 // 最大读取字节数 int main(int argc, char *argv[]) { int sockfd, n; struct sockaddr_in servaddr; char buffer[MAXLINE]; // 创建 socket sockfd = socket(AF_INET, SOCK_STREAM, 0); // 初始化服务器地址结构体 bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8080); // 将 IP 地址转换网络字节序 inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 连接服务器 if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { printf("Connection error.\n"); return 1; } // 输入用户名 printf("Username: "); bzero(buffer, MAXLINE); fgets(buffer, MAXLINE, stdin); // 向服务器发送用户名 write(sockfd, buffer, strlen(buffer)); // 读取服务器返回的信息 bzero(buffer, MAXLINE); read(sockfd, buffer, MAXLINE); printf("%s", buffer); // 读取服务器返回的时间信息 bzero(buffer, MAXLINE); read(sockfd, buffer, MAXLINE); printf("Server time: %s", buffer); // 关闭连接 close(sockfd); return 0; } ``` 注意:代码中的服务器 IP 地址为本地回环地址 127.0.0.1,端口号为 8080。在实际使用中需要根据实际情况修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

futureCode.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值