linux下网络编程

Linux环境下使用套接字进行进程之间的通信。通过套接字接口,其他进程的位置对于应用程序来讲是透明的。套接字代表通信的端点,也就是说两个端点应该各有一个套接字才可以。两个套接字建立了通信双方的桥梁,而套接字就是这座桥的入口,如图所示。

套接字同样拥有一个套接字描述符,应用程序可以像操作文件一样操作一个套接字。因此在进行网络通信的过程中,用户感觉就好像在操作一个文件一样。这正是Linux将外部设备抽象为一个文件的好处,如图所示。



每一台主机由于体系结构的不同,所采用的数据存储方式也不相同。在网络环境中,进程之间的通信是要跨越主机的,这时就有了一个字节序不统一的问题。为了解决这个问题,网络协议提供一种字节序,当跨越主机的两个进程进行通信时,先将需要传输的数据转换成网络字节序,待接收方接收数据后,将其转换为本地主机的字节序,如图所示。

Linux环境下使用以下四个函数进行字节序之间的转换,其函数原型如下:

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
uint16_t htons(uint16_t hostint16);
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t nettint16);

htonl函数的参数是一个32位的本地主机数据,该数据采用的是主机字节序。htonl函数将其转换为网络字节序,并且返回。htons函数和htonl函数功能类似,不过该函数对16位整数进行转换。ntohl函数和ntohs函数可以看做是前面两个函数的逆函数,其功能是将网络字节序的数据转换为主机字节序。

Linux中使用in_addr结构表示一个IP地址,该结构的定义如下:

#include <netinet/in.h>
struct in_addr{
in_addr_t s_addr;     /* in_addr_t被定义为无符号整型 */
}

Linux中的地址结构除了上述的IP地址和端口号外,还有一些额外的信息,该结构定义如下:

#include <netinet/in.h>
/* Linux中的网络通信地址结构 */
struct socketaddr_in{
sa_family_t sin_family;        /* 16位的地址族 */  一般为AF_INET、AF_INET6
in_port_t sin_port;            /* 16位的端口号 */
struct in_addr sin_addr;        /* 32位的IP地址 */
unsigned char sin_zero[8];    /* 填充区,8个字节填“0” */
}

网络通信中地址族统一使用AF_NET。
最后一个成员表示的是填充区,其作用是保证socketaddr_in结构正好16个字节。
这样做的目的是为了使socketaddr_in结构可以和socketaddr地址结构随意转换。

socketaddr结构定义如下:

#include <netinet/in.h>
struct socketaddr{
sa_family sa_family;    /* 16位的地址族*/
char sa_data[14];        /* 14字节的填充区 */
}

这两个结构定义为等长,于是在使用时就可以方便地使用强制类型转换,使数据在这两个结构之间转换,socketaddr结构中的sa_data成员实际可以看成是由sin_port、sin_addr和sin_zero三个成员变量组成的。

 IP地址是以二进制数的形式存储在地址结构中的,需要输出并观察该地址时未免有些不便,对于程序员来讲,点分十进制(xxx.xxx.xxx.xxx)表示的IP地址才更直观。
因此Linux提供了IP地址的格式转换函数。
#include <netinet/in.h>
const char * inet_ntop(int domain, const void * restrict addr, char * restrict str, socklen_t size);
int inet_pton(int domain, const char * restrict str, void * restrict addr);

这两个函数中都有domain参数,该参数的值可以是AF_INET或者AF_INET6,这两个值分别定义在netinet/in.h文件中,表示IPv4协议IPv6协议。协议的不同导致地址长度的不同,所以参数addr表示二进制形式的IP地址,其类型却不能简单地定义成32位整型或者128位的整型。参数str指向的区域存储IP地址的点分十进制表示形式的字符串,参数size表示的是该区域的大小,以防止因为地址格式不对导致的越界访问。因为IP地址的字符串形式包含四个“.”和一个‘\0’结束符,所以存储该字串的缓冲区最大应为:3个字符数字 * 4 + 3个字符‘.’+ ‘\0’共16字节。

 一台主机有许多和网络相关的信息,例如,主机名称、IP地址、主机所提供的服务等。这些信息一般都保存在系统中的某个文件里(例如/etc/hosts等),用户程序可以通过系统提供的函数读取这些文件上的内容。Linux环境下使用gethostent函数读取和主机有关的信息,该函数的原型如下:
#include <netdb.h>
struct hostent * gethostent(void);

该函数从系统的/etc/hosts文件中读取主机相关信息,并将其内容存储在系统中的一个静态缓冲区中,返回该静态缓冲区的首地址;如果失败则返回NULL。该结构定义在netdb.h文件中,其原型如下:

#include <netdb.h>
struct hostent{
     char * h_name;        /* 正式主机名,每个主机只有一个 */
      char **h_aliases;    /* 主机别名列表,可以有很多个,以二维数组形式存储 */
      int h_addrtype;        /* IP地址类型,可以选择IPv4或者IPv6 */
      int h_length;        /* IP地址长度,IPv4对应4字节的地址长度 */
      char **h_addr_list; /* IP地址列表,h_addr_list[0]为主机的IP地址 */
};
对于用户来说套接字的地址结构的信息是不必要的,用户只需传递一个sockaddr_in地址结构的地址,之后由系统填充其中的内容即可。这种细节的屏蔽是有意义的,这样可以减少出错的概率,使程序的逻辑更加简单明了。
网络环境中的服务器需要提供一个唯一的IP地址和一个主机名(域名)。对于大多数的服务器来说,客户端往往不知道其准确的IP地址,更多的只知道其域名。例如,www.baidu.com就是一个域名,用户需要访问该服务器时不知道其具体IP地址。在这种情况下用户可以求助于DNS服务器。该服务器可以将域名转换为IP地址转换后的IP地址和端口号存储在一个名为addrinfo的地址信息结构中。

Linux环境下提供一个函数,根据用户指定的服务器域名和服务名称得到服务器的IP地址和端口号。
并将其填写到一个sockaddr_in地址结构中。该函数内部访问了DNS服务器,从而可以得到需要访问主机的IP地址和端口号。

其函数原型如下:

#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *restrict host, const char *restrict service,
const struct addrinfo * restrict hint, struct addrinfo **restrict res);

getaddrinfo函数的前两个参数分别表示需要访问的主机名和服务的名称。
注意这两个名称都应当已经在DNS服务器中注册。
第3个参数表示一个过滤地址模板,通常情况下不过滤任何IP地址,因此将该参数设置为NULL。
第4个参数表示一个地址信息结构的列表。
该列表列出了所有可用的符合条件的地址结构,用户可以从中任选一个作为通信的地址。
如果成功得到地址信息列表,getaddrinfo 函数返回0,失败则返回–1。

套接字是通信端点的抽象。在网络编程过程中,套接字对大多数通信细节做了隐藏,使程序员操作起来像操作文件一样简单,也正因为如此,许多文件操作的函数都可以应用在套接字上。由此可见,Linux将设备抽象为文件的策略着实使Linux环境下的编程简便了不少。
当需要进行网络通信时,第一件事情就是要创建一个用于通信的套接字。

Linux环境下使用socket函数创建一个套接字,其函数原型如下:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

socket函数的第1个参数表示的是通信域:
AF_INET 表示使用ipv4协议
AF_INET6 表示使用ipv6协议
AF_UNIX 表示使用非网络的进程间通信
AF_LOCAL 同上
AF_UNSPIC 未制定域
socket函数的第2个参数表示套接字的类型:
SOCK_DGRAM 长度固定的,无连接报文 UDP
SOCK_RAM 原始套节字 绕过协议
SOCK_SEQPACKET 长度固定的,面向连接的报文 STCP
SOCK_STREAM 有序的,面向连接的字节流 TCP
第3个参数protocol制定一个协议。这个参数通常为0,表示使用默认的协议。

需要注意的是:
SOCK_SEQPACKET和SOCK_STREAM两种套接字类型很像,
区别在于前者使用的是有序可靠的报文,而后者使用的是有序可靠的字节流。
SOCK_RAW表示原始套接字,使用这种套接字时需要用户自己构造协议头部,
也就是说用户可以绕过协议,直接访问下面的网络层。

如果不需要使用时,使用close函数即可关闭一个套接字,像关闭文件一样。

#include <sys/socket.h>
……
int fd;
fd = socket(AF_INET, type, 0); /* 创建一个套接字 */
……         /* 对套接字进行操作 */
close(fd); /* 关闭套接字,像关闭文件一样 */
 只有绑定了地址的套接字才能用于网络通信,因此只创建一个套接字是远远不够的。如果想使用这个套接字,则应当将其与一个地址绑定。

Linux环境下使用bind函数将一个套接字绑定到一个地址上,其函数原型如下:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * addr, socklen_t len);

第1个参数是一个已经创建的套接字描述符。
第2个参数是一个地址结构,其声明的原型通常是struct sockaddr * addr,但是在实际编程中通常使用struct sockaddr_int *addr。
因此,在第二个地址结构参数的时候需要做一个强制类型转换。

struct sockaddr_in * addr;
/* 设置地址结构 */
addr = (struct sockaddr_in *)malloc(sizeof(struct sockaddr_in));
addr->sin_family = AF_INET;        /* 使用IPv4协议的地址族 */
addr->sin_port = 1024;            /* 端口设置为1024 */
addr->sin_addr = 0x60ba8c0;          /* 32位的IP地址,即192.168.11.6,注意应使用网络字节序进行赋值 */
bind(fd, (struct sockaddr_in )addr, sizeof(struct sockaddr_in));        /* 地址绑定,转换addr指针的类型 */

通常来说应该避免手动对地址结构的成员变量进行赋值,因为这样做需要注意的细节太多。首先,协议族必须和使用的地址结构匹配,例如,如果使用sockaddr_in结构,那么通信域就不能指定为AF_INET6,因为IPv6协议的地址结构占用28个字节,而IPv4协议的地址结构占用16个字节。其次,端口号应该是一个大于1024的无符号整数,因为只有根用户的权限可以使用1024以下的端口号。通常这个端口号应该由系统指派,因为该端口号还有可能被其他进程占用。最后,对于IP地址的赋值很麻烦,程序员习惯于更直观的点分十进制表示IP地址,可是机器必须接受一个二进制形式的地址。综上所述,不直接对sockaddr_in结构赋值是一个比较可靠的选择。如果绑定成功,bind函数返回0;如果失败则返回-1。

客户端的程序也可以不期望特定的服务器端,而是希望可以接收到网络中任意主机的数据包,这时可以将IP地址,也就是地址结构中的sin_addr设置为INADDR_ANY。
这样,客户机就可以接受网络中任意的数据包了。

struct sockaddr_in * addr;
addr = (struct sockaddr_in *)malloc(sizeof(struct sockaddr_in));
addr->sin_addr = INADDR_ANY

当一个套接字绑定了地址后,就可以使用其建立一个连接了。对于面向连接服务的套接字类型,例如,SOCK_STREAMSOCK_SEQPACKET,这一步操作是必须的,无连接服务的套接字则没有这个必要。Linux环境下使用connect函数主动建立一个连接,其函数原型如下:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr * addr, socklen_t len);

第1个参数是一个已经创建好的套接字描述符。
第2个参数是需要通信的地址,因为服务器程序只有一个,而客户端程序有成千上万个。所以一般而言,服务器程序会等待客户端程序的连接而不会主动连接客户端。因此对于大多数情况来说,这个参数是服务器端的地址结构,它包括IP地址和服务所对应的端口号,通常通过getaddrinfo函数映射而来。
第3个参数是地址结构的长度,对于IPv4协议的地址族来说,其长度为16个字节。

客户端主动建立一个连接,服务器端就要能够接受一个连接。服务器端接受连接分为2步,第1步创立一个套接字监听连接请求,第2步创立一个新的套接字接受并且处理请求。注意,这2步使用的是两个不同的套接字描述符。Linux环境下使用listen函数监听客户端的连接请求,其函数原型如下:

#include <sys/socket.h>
int listen(int sockfd, int backlog);

第1个参数是要用来进行连接请求监听的套接字描述符。
第2个参数表示最多可以排队等待连接的请求数量。
调用listen函数之后,该套接字就可以开始监听请求了,所以称此套接字为监听套接字。当监听就绪后,就需要服务器程序进行下一步:接受连接请求。
Linux环境下使用accept函数接受一个连接请求,其函数原型如下。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr * addr, socklen_t len);

第1个参数是一个创建的套接字描述符,该套接字通常就是监听套接字。当连接请求未到时,accept函数会造成服务器程序的阻塞。当接受到一个连接请求时,监听套接字会创建一个新的套接字,该套接字拥有和监听套接字相同的地址和套接字类型。这个新套接字称为链接套接字,会完成与发出请求的客户端(调用connect函数的客户端程序)的连接,并从accept函数中返回,服务器程序继续运行,开始进行真正的通信。而监听套接字可以继续保持监听其他的连接请求。
第2个参数是一个地址结构,该地址往往是客户端的地址。在客户端程序调用connect函数之后,其地址回应并发送至服务器。服务器程序可以提供一个缓冲区从内核接收此地址,也可以忽略它,将该参数置为NULL。
第3个参数表示取得的客户端地址的大小,这个值要小于用户提供的缓冲区的大小。如果忽略该地址,则该参数为NULL。

struct sockaddr_in addr;
socklen_t len;
listen(sockfd, 10);            /* 开始监听连接请求 */
accept(fd, (struct sockaddr )&addr, &len);    /* 等待并且接受一个请求,addr和len用于保存客户端地址和该地址结构的长度 */

accept(fd, NULL, NULL);        /* 另一种写法,忽略客户端的地址 */

如果成功建立了连接,accept函数返回0,如果失败则返回–1。当accept函数返回时,服务器和客户端的连接就已经建立了,依赖于该连接的通信就可以开始了,用户可以像操作文件一样操作套接字:使用read函数和write函数进行读写;使用dup函数将套接字重定向到标准输入输出;如果不需要使用则将套接字关闭,这时已经建立的连接就自动断开了。注意不能使用lseek对套接字描述进行文件定位。

TCP服务器的编程流程是:
1、创建套节字
2、绑定套节字
3、设置套节字为监听模式,进入被动接受连接装态
4、接受请求,建立连接
5、读取数据
6、终止连接
用代码表示为:

int main(void)
{
socket(...);         /* 步骤1创建套接字 */
bind(...);             /* 步骤2绑定地址和端口号 */
listen(...);         /* 步骤3启动监听 */
while(1) {             /* 服务器程序多是驻留程序 */
accept(...);     /* 接受并处理一个连接请求 */
while(1) {         /* 如果客户端程序是一个交互式程序,这里又出现一个死循环 */
read(...);
process(...); /* 与客户端交互,客户端与用户交互,直到用户退出客户端 */
write(...);
}
close(...);`     /* 客户端退出,关闭连接,通信结束 */
}
return 0;
}

TCP客户端编程流程:
1、创建套节字
2、与远程服务器建立连接
3、读取数据
4、终止连接
用代码表示为:

int main(void)
{
socket(...);         /* 步骤1创建套接字 */
bind(...);             /* 步骤2绑定地址和端口号 */
connect(...);        /*步骤3与远程服务器建立连接
write(...);
read(...);
close(...);               /* 退出,关闭连接,通信结束 */
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值