世界微尘里,吾宁爱与憎
Socket编程(C版)
Socket编程(C版)

Socket编程(C版)

@Author:清秋橘 Autumn_Tangerine

@Date:2021/7/18

@Last Modified:2021/7.18

序言

最近跟着学校老师在做一个上线的项目,时间紧任务重,需要尽量在这个暑假前就把大概的东西都写好,因为主要涉及到了将VB/VC代码迁移到QT for Linux上,其中大部分为Socket网络编程操作,之前接触到的一部分网络编程知识渐渐有点不够用弄了,所以通过各种渠道把C的网络编程重新学了一遍

发现网上关于这部分的博客其实并不是很多而且质量都不高,所以打算自己写一篇,主要供自己以后复习用

如果很幸运的这篇博客可以被其他人读到,那就一起努力吧

在阅读此博客之前,为了确保你可以顺利读懂该博客,你需要具备以下方面的知识

  • 对计算机网络的基本了解,至少需要对TCP/IP协议及应用层协议有基本的认识
  • C/C++编程基础
  • Linux/Windows系统操作(非必要)

关于计算机网络这部分的相关知识,可以看我另一篇博客《花七天时间,从流程上总结贯通整个计算机网络知识》

本博客主要基于IPV4和Linux平台的Socket编写,其他部分仅有少量涉及,仅供参考

理解Socket

在计算机网络基础课中学到过,Socket是IP地址+端口号,比如{192.168.1.1 :: 8888},但是我觉得这个描述并不是十分的恰当

Socket又称为套接字,插座,运行在计算机中的两个程序通过Socket建立起一个通道,数据在通道中进行传输,它把复杂的TCP/IP协议族隐藏了起来,对于C++网络编程来说,Socket是其自带的一个库,这个库里面封装了传输数据或者接受数据时用到的各种操作,对于用于而言,进行数据传输便不用对底层TCP/IP直接进行复杂的操作,而只需要用Socket相关的函数,就可以完成网络通信

相关的库函数

基于Linux平台的Socket库

socket

int socket(int domain, int type, int protocol);

socket函数用于创建一个新的socket,也就是向系统申请一个socket资源,或者这里就可以按照计算机网络基础课中那样理解,socket函数就是像系统申请到了一个IP地址+端口号

参数说明:

  • domain:协议域,又称协议族(family)。常用的协议族有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等,这里经常用到的是前两种
  • type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。
  • protocol:指定协议,常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

返回值:

成功则返回一个socket,失败返回-1,错误原因存于errno 中,除非系统资料耗尽,socket函数一般不会返回失败

一般来说,创建Socket第一个参数填AF_INET,第二个参数填SOCK_STREAM,第三个参数填0

这里第三个参数填的意思是IPPROTO_IP,即接收任何的IP数据包。其中的校验和和协议分析由程序自己完成,而如果你选择了比如IPPROTO_TCP,那么,他就只会接受TCP数据包

gethostbyname

struct hostent *gethostbyname(const char *name);

把ip地址或域名转换为hostent 结构体表达的地址

参数说明:

name,域名或者主机名,例如”192.168.1.3″、”www.autumn-tangerine.cn“等

返回值:

如果成功,返回一个hostent结构指针,失败返回NULL。

inet_addr

in_addr_t inet_addr(const char *cp);

参数说明:

传入参数为”a.b.c.d”格式时,inet_addr()会检查abcd每个字段是否>255,如果有>255的字段,则返回INADDR_NODE

但当传入参数为”a.b.d”、“a.d”、”d”时,inet_addr()会把除d外,其余字段从前往后填入点分十进制对应字段,d则用来补完剩下的字段

另外,inet_addr()支持字段使用8进制和16进制

在这里插入图片描述

换句话说,这个函数主要的作用就是将字符串格式的IP地址转换整数类型地址

返回值:

如果正确执行将返回一个无符号长整数型数。如果传入的字符串不是一个合法的IP地址,将返回INADDR_NONE。

在这里插入图片描述

inet_pton()

int inet_pton(int af, const char *src, void *dst);

参数说明:

  • af:地址簇
  • src:来源地址
  • dst:用于接收转换后的数据

inet_pton()对于传入的参数只支持”a,b,c,d”格式,同时不支持8进制及16进制输入

这个函数转换字符串到网络地址,是inet_addr的扩展

af = AF_INET时,src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中。

af = AF_INET6时,src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中。

返回值:

如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0

inet_addr()相比于inet_pton()更为宽松,但inet_pton支持IPV6,inet_addr()有时不符合我们对地址格式的要求,这时可以使用inet_pton()对传入的格式进行有效的判断

int check_ip(const char *ip)
{
   struct in_addr p;
   int ret = inet_pton(AF_INET, ip, &p);
   if (ret == 0 || errno == EAFNOSUPPORT) {
       return -1;
  }

   return 0;
}

主机字节序和网络字节序

首先,我们应该先搞清楚字节顺序是一个什么概念

字节顺序是指战内存多于一个字节类型的数据在内存中的存放顺序,比如一个32位整数由4个字节组成,内存中存储这4个字节有两种办法,一种是将底序字节存储在起始位置,这成为小端字节序,另一种方法是将高序字节存储在起始位置,这成为大端字节序

而在不同的主机当中,由于CPU的设计不同,就算是在同一操作系统中,甚至是在同一机器上的两个进程(C语言与Java采用不同的字节序),数据存储的字节顺序仍然有可能不同,而如果要求数据能够在不同的两台主机之间通信的话,就需要沟通好一致的字节顺序,所以,在网络字节序中,为了保证数据在不同主机之间传输时能够被正确解释,网络字节序统一规定了字节顺序为大端字节序

所以,在进行通信之前,不管主机采用什么字节顺序,都需要将主机字节序先转换为网络字节序,其过程中涉及到的转换函数:

htons,htonl = host to network short/long

htonl()函数的格式为

u_long htonl(u_long hostlong);

其中,参数hostlong是主机字节序的数据,该函数的返回值是转换之后的网络字节序的数据

htons()函数的格式为,z

u_short htons(u_short hostshort);

该函数的用法与htonl()类似,htonl用于32位无符号数的转换,htons用于16位无符号数的转换

常用结构体

sockaddr

struct sockaddr{
   unsigned short sa_family;//地址族,AF_xxx   AF_INET 不涉及转序的问题
   char sa_data[14];        //14字节的协议地址(端口+地址),网络字节序
};

sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET sa_data是14字节协议地址

此结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。但因为该结构中端口号与地址在同一个变量sa_data中存储,不便于操作,所欲实际编程中并不直接针对此数据结构操作,而是使用另一个与sockaddr等价的数据结构,sockaddr_in

sockaddr_in

struct sockaddr_in { 
    short int sin_family; //地址族
    unsigned short int sin_port; //端口号
    struct in_addr sin_addr; //Internet地址
    unsigned char sin_zero[8]; //为了保持struct sockaddr 一样的长度,16个字节
}; 

sin_port存储端口号(使用网络字节顺序) sin_addr存储IP地址,使用in_addr这个数据结构 sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

此时:

struct in_addr{
	union{
	struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un_b;
	struct{u_short s_w1,s_w2;}S_un_w;
	u_long S_addr;//网络字节顺序
	}S_un;
};

这里涉及到union(联合)的概念,它是一种特殊的类。通过关键字union进行定义,一个union可以有多个数据成员,在任意时刻,联合中只能有一个数据成员可以有值。当给联合中某个成员赋值之后,该联合中的其它成员就变成未定义状态了

s_addr按照网络字节顺序存储IP地址

S_un_b和S_un_w分别为把IP地址且切分为四个char字符存入和将IP地址切分为两个无符号short类型存入,因为这俩种方式不是很常用,所以也可以理解为:

struct in_addr{
	u_long S_addr;//网络字节顺序
};

所以同时也有:

struct sockaddr_in { 
    short int sin_family; //地址族
    unsigned short int sin_port; //端口号
    u_long S_addr; //Internet地址
    unsigned char sin_zero[8]; //为了保持struct sockaddr 一样的长度,16个字节
}; 

当然,这样写只是便于理解,实际上sockaddr_in中并没有S_addr这个变量

sockaddr与sockaddr_in二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr

sockaddr_in6

struct sockaddr_in6{
 __SOCKADDR_COMMON (sin6_);
 in_port_t sin6_port;      
 uint32_t sin6_flowinfo;    
 struct in6_addr sin6_addr; 
 uint32_t sin6_scope_id;    
}

IPV6结构体,不做过多介绍

hostent

struct hostent  
{  
    char *h_name;         //正式主机名  
    char **h_aliases;     //主机别名  
    int h_addrtype;       //主机IP地址类型:IPV4-AF_INET  
    int h_length;         //主机IP地址字节长度,对于IPv4是四字节,即32位  
    char **h_addr_list;   //主机的IP地址列表  
}; 
  • h_name,它是一个字符指针,指向主机名
  • h_aliases是一个二重指针,指向主机的别名,一个主机可以有多个别名,h_aliases可以用下标的方式访问不同的主机别名
  • h_addrtype,为int型数据,指定主机使用的IP地址类型
  • h_length指定IP地址长度
  • h_addr_list也是一个二重指针,与h_aliases一样,也可以通过下标进行访问。它指定主机的不同IP地址,在实际的应用中,一台服务器往往有好几个IP地址,而域名只有一个,这样设计的好处是,可以使系统分布设计,提升服务器的稳定性和抗灾难能力。一般对服务器的访问,则是先经过DNS服务器,DNS通过均衡设计,返回合适的IP与客户端进行交互,避免客户端只连接一个IP,导致网络拥堵

从网上搜寻了很多资料,包括CSDN,关于这部分的讲解很乱,无非就是你抄我我抄你,根本没有点实质性的东西,所以这里的东西加入了一点自己的理解

个人认为,hostent可以理解为存储一个主机信息的结构体,比如对于百度来说

img

正式主机名为www.a.shifen.comwww.baidu.com为别名,

参考资料

本博客在编写时参考到了以下网络或书籍上的资源,感谢

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注