套接字(socket)编程简介

套接字(socket)编程简介


现在的网络编程几乎都是用的socket。

我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?

这些都得靠socket!那什么是socket?下面介绍一下socket的相关概念和一些基本函数。

套接字概念

Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。

既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

套接字通信原理如下图所示:
在这里插入图片描述

在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。

TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。本文的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX Domain Socket的函数接口。

应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为**套接字(Socket )**的接口,区分不同应用程序进程间的网络通信和连接。

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用以下模式来操作

打开open –> 读写write/read –> 关闭close”

Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

生成套接字,主要有3个参数:通信的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号

Socket 原意是“插座”。通过将这3个参数结合起来,与一个“插座”Socket 绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层

在这里插入图片描述

sockaddr数据结构

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

在这里插入图片描述

sockaddr数据结构:

struct sockaddr {
	sa_family_t sa_family;   //地址结构类型
	char sa_data[14];     //地址数据, 14 字节的协议地址,sa_data则包含该socket的IP地址和端口号 
};

/*说明:
在实际编程中,一般定义struct sockaddr_in  addr,
然后给各个成员赋值,传参数强制转换为struct sockaddr, 例如 (struct sockaddr *) &addr*/

IPv4: struct sockaddr_in (internet), 16个字节

struct sockaddr_in {
	__kernel_sa_family_t sin_family;      //地址结构类型,AF_INET
	__be16 sin_port;               //端口号
	struct in_addr sin_addr;          //IP地址
    
 	/* Pad to size of `struct sockaddr'. */
 	unsigned char sin_zero[sizeof (struct sockaddr) -
                           sizeof (sa_family_t) -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];  
};

//其中ip地址封装了32位的地址信息--对应点分十进制
struct in_addr {          
	__be32 s_addr;
};

IPv6: struct sockaddr_in6, 28个字节

struct sockaddr_in6 {
	unsigned short int sin6_family;     //地址结构类型,AF_INET6
	__be16 sin6_port;          //端口号
	__be32 sin6_flowinfo;        //流量信息
	struct in6_addr sin6_addr;     //IP地址
	__u32 sin6_scope_id;        //scope_id
};

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
};

struct sockaddr_un, 110字节

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

网络字节序与主机字节序

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

在这里插入图片描述

网络字节序:网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以,在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。

字节顺序转换函数

头文件:#include <arpa/inet.h>

·htonl():把32位值从主机字节序转换成网络字节序
·htons():把16位值从主机字节序转换成网络字节序
·ntohl():把32位值从网络字节序转换成主机字节序
·ntohs():把16位值从网络字节序转换成主机字节序

1. uint32_t htonl(uint32_t hostint32);

功能:

将 32 位主机字节序数据转换成网络字节序数据

参数:

hostint32:需要转换的 32 位主机字节序数据,uint32_t 为 32 为无符号整型

返回值:

成功:返回网络字节序的值

2. uint16_t htons(uint16_t hostint16);

功能:

将 16 位主机字节序数据转换成网络字节序数据

参数:

hostint16:需要转换的 16 位主机字节序数据,uint16_t,unsigned short int

返回值:

成功:返回网络字节序的值

3. uint32_t ntohl(uint32_t netint32);

功能:

将 32 位网络字节序数据转换成主机字节序数据

参数:

netint32:待转换的 32 位网络字节序数据,uint32_t,unsigned int

返回值:

成功:返回主机字节序的值

4. uint16_t ntohs(uint16_t netint16);

功能:

将 16 位网络字节序数据转换成主机字节序数据

参数:

netint16:待转换的 16 位网络字节序数据,uint16_t,unsigned short int

返回值:

成功:返回主机字节序的

IP地址转换

头文件:

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>

1. int inet_pton(int family, const char *strptr, void *addrptr);

功能:

将点分十进制数串转换成 32 位无符号整数

参数:

family:协议族( AF_INET、AF_INET6、PF_PACKET 等 ),常用 AF_INET

strptr:点分十进制数串

addrptr:32 位无符号整数的地址

返回值:

成功返回 1 、 失败返回其它

2. const char *inet_ntop( int family, const void *addrptr, char *strptr, size_t len );

功能:

将 32 位无符号整数转换成点分十进制数串

参数:

family:协议族( AF_INET、AF_INET6、PF_PACKET 等 ),常用 AF_INET

addrptr:32 位无符号整数

strptr:点分十进制数串

len:strptr 缓存区长度

len 的宏定义

#define INET_ADDRSTRLEN 16 // for ipv4

#define INET6_ADDRSTRLEN 46 // for ipv6

返回值:

成功:则返回字符串的首地址

失败:返回 NULL

3. in_addr_t inet_addr(const char * cp)

inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char *cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-1,255.255.255.255是一个有效的地址,不过inet_addr无法处理

4. char *inet_ntoa(struct in_addr in)

inet_ntoa 函数转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(复盖)

应用举例:

#include <sys/types.h> 
#include <sys/socket.h> 
#include <arpa/inet.h> 

int a = 0x01020304; 
short int b = 0x0102;    
printf("htonl(0x%08x) = 0x%08x\n", a, htonl(a)); 
printf("htons(0x%04x) = 0x%04x\n", b, htons(b));
 
char ip_str[]="172.20.223.75"; 
unsigned int ip_uint = 0; 
unsigned charchar *ip_p = NULL; 
inet_pton(AF_INET,ip_str,&ip_uint); 
printf("in_uint = %d\n",ip_uint); 
 
unsigned char ip[] = {172,20,223,75}; 
char ip_str[16] = "NULL"; 
inet_ntop(AF_INET,(unsigned intint *)ip,ip_str,16); 
printf("ip_str = %s\n",ip_str); 

strcut sockaddr_in  add; 
add.sin_addr.s_addr  = inet_addr("*.*.*.*"); //构建网络地址。 
printf("ip is %s\n",inet_ntoa(add.sin_addr)); 

char *add1,add2; 
src.sin_addr.s_addr = inet_addr("192.168.1.123"); 
add1 =inet_ntoa(src.sin_addr);          
src.sin_addr.s_addr = inet_addr("192.168.1.124"); 
add2 = inet_ntoa(src.sin_addr); 

总结:

struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式。这两个结构体一样大,都是16个字节,而且都有family属性,不同的是:

sockaddr用其余14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero

分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。

sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别:

程序员不应操作sockaddr,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中,sockaddr是给操作系统用的

程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便

字的地址形式。这两个结构体一样大,都是16个字节,而且都有family属性,不同的是:

sockaddr用其余14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero

分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。

sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别:

程序员不应操作sockaddr,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中,sockaddr是给操作系统用的

程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值