套接字与网络通信

来源:互联网 发布:淘宝茶叶有多少人竞争 编辑:程序博客网 时间:2024/05/17 02:39

进程之间有很多的通信方法。 例如管道(包括有名管道和无名管道), 用于异步通信的信号机制,  System V进程间通信(包扩信号量, 消息队列, 共享内存)。 这些通信机制只是适用于单个机器内部的进程之间进行通信。 

这里我们说的是跨主机的进程之间的通信机制, 即基于BSD的socket(套接字)通信(即常说的网路通信), 它不仅支持本地无关联的两个进程之间的通信, 还支持跨网络的, 不同主机之间的进程之间的通信。 利用套接字, 我们很容易的实现分布与网络的C/S等模型。

讨论的网络中两主机之间通信拓扑如下:


一 TCP/IP 模型

TCP/IP 是计算机网络通信的一组协议, 又被称为TCP/IP 协议簇, 主要包括TCP, IP, UDP, ICMP 等协议。 TCP/IP 有如下四层结构:自上而下分别为应用层, 传输层, 网络层, 网络接口层。

(1)网络接口层

主要用于数据帧的发送和接受工作。 所谓的帧(frame)就是网络信息传输单元。 也就是说该层负责将帧放到网络中, 或者从网络中把帧接收下来。 这一层主要是一些设备驱动程序和网卡等硬件组成。

(2)网络层

这一层主要是实现了一组网络互连协议。传输的数据是IP报文, 每个IP报文都有目的地址和源地址。 用于把一个包(package)从发送方主机经过网络, 传到另一个具有IP地址的主机上。也就是说, 负责报文的路由选择。 最核心的协议就是IP(internet protocol)协议了。  这一层实现了两个IP协议版本, 一个是IPv4(32 bit), 一个是IPv6(128 bit)。 除此之外, 还有用于差错诊断的ICMP(internet control message protocol)协议, 以及用于相邻的多播路由(multicast routers)用于建立多播组的IGMP(internet group management protocol)协议。

(3)传输层

传输层主要为应用层提供end-to-end(or host-to-host)的服务。  主要有两个传输控制协议:

一  TCP(transmission control protocol)协议, 这个协议时connection oriented transmissions(面向连接的), 也就是先连接上, 然后在进行数据传输。  实现的是可靠传输。适合一次传输大批数据, 并适用于得到响应的应用程序 。

二 UDP(user datagram protocol)协议, 提供的是无连接的通信, 且不对传送包进行可靠性确认, 适合于一次小批数据的传输, 可靠性有应用层完成。 


(4)应用层

应用程序通过这一层访问网络。 主要包括如下协议:

一 Telnet:  用于远程登录服务

二  FTP 用于文件传输

三 SMTP(simple mail transfer protocol) 用于电子邮件协议

四 DNS(domain name system)  用于域名解析服务, 即将域名映射为IP地址的协议

五 FTTP 用于超文本传输协议。 作用就相当于C/S的请求-响应协议。 例如一个浏览器, 就是一个client, 提交一个HTTP的请求message到一个server, server将返回HTML文件以及其他的内容给client。

数据包的封包拆包过程:


IP 地址

IP地址是在逻辑上唯一的标识一台主机, 为32位的。 MAC地址是在物理上唯一的标识一台主机的, 为48bit的. 端口号是主机内唯一标示运行进程的。

一个IP地址由网络号和主机号两部分组成。  

网络ID: 标识一个网络。 同一个网络上的主机使用同一个网络号。 拥有相同的网络号的主机之间通信不需要经过路由设备。 这或许解释了同一个实验室的两台主机之间传送文件速度很快吧。

主机ID: 对于一个网络号来说,  其内部的每一台主机的主机号是唯一的。 每一个主机由一个逻辑IP地址确定网络号和主机号。 

为适应不同大小的网络, 定义了如下五类IP地址(有二进制表示法和点分十进制表示法(最常见)): A, B, C, D, E类地址。

A类地址一般分配给政府机构。 拥有最大数量的主机。 最高位为0, 紧跟的7位为网络号, 最后24位表示主机号。 总共有126个网络。 为什么不是127, 因为最后一个网段127(即0 1111111)适用于本地回环测试的。  广播地址为X.255.255.255 , 点分十进制第一个字节的范围为1-126

B类地址, 最高两位置为10, 前16位为网络号。 广播地址是X.X.255.255 ,点分十进制第一个字节的范围为128-191

C类地址, 最高三位置为110, 前24位总为网络号, 后八位为主机号。 广播地址为X.X.X.255 。

注意广播地址不能被当做主机号。点分十进制第一个字节的范围为192-223.

D类地址, 最高位总被置为1110, 用于组播通信。 没有网络号和主机号之分。点分十进制第一个字节的范围为224-239.

E类地址: 最高位总被置为1111. 仅供实验的地址. 点分十进制第一个字节的范围为240-254.


子网掩码:

是用于区分网络号和主机号的一个32位的地址。 屏蔽掉主机号(与主机的IP地址相与)。 默认子网掩码如下:

A类地址掩码: 255.0.0.0

B类地址掩码: 255.255.0.0

C类地址掩码: 255.255.255.0


可以选择新的子网掩码重新划分子网。 例如对255.255.224将C类地址103.67.10.0这个网络分成8组子网。 如下
255.255.255.224对应的二进制表示为如下:

11111111  11111111  11111111  11100000.

屏蔽掉的时主机号, 网络号留下来了。 所以111共有8种可能。从000到111.


套接字(Socket)


套接字是应用程序和下层网络协议层之间的一个通信端口。 对程序员来说, 套接字就等价于网络。

使用套接字通信必须首先创建套接字。 互相通信的两个进程必须都调用socket()来创建自己那一端的套接字:

<span style="font-size:18px;">#include <sys/socket.h>int socket(int domin, int type, int protocol);</span>


上述函数在通信域domain中创建一个类型为type, 使用协议为protocol的套接字, 并返回一个最小的未使用的描述字。

一个给定的描述字要么代表一个打开的文件, 要么代表一个套接字。

套接字对应三种属性: 域, 类型,协议。

参数domain指明通信域。 通信域决定了使用的网络协议。 头文件<socket,h>列出了系统支持的通信域:

(1)AF_UNIX: UNIX通信域, 即同一台计算机内两个进程通过文件系统进行通信。 套接字的地址就是文件系统的路径名。

(2) AF_INET: 网络通信, 使用32位的IPv4地址

(3) AF_INET6: 网络通信, 使用128位的IPv6地址、

 

参数type指明套接字的类型, 主要有三种socket类型:
(1)SOCK_STREAM:  字节流套接字, 简称流套接字, 是面向链接的, 双向可靠的通信。 如TCP

(2)SOCK_DGRAM:  数据报套接字。 支持双向通信。 但是此类套接字是不可靠的。 不保证数据报是顺序的, 可靠地, 不重复的。 不同的数据报可以采用不同的路由器到达目的地址。如UDP

(3)SOCK_RAW: raw套接字。 只有根用户才能创建raw套接字。

 

第三个参数标识采用协议簇的哪一种协议。 如果设置为0, 就是让系统自动选择默认协议。 但是对于raw 套接字(原始套接字), 必须有用户指明使用哪一个具体的协议。

 

 

一般而言, 一旦指明了通信域和通信类型, 协议就是唯一的。 因此大部分情况下都设置protocol为0, 即让系统选择默认协议。

socket的这三种参数不能随意指定组合值。 例如Unix域就不支持raw套接字类型。

domain和type 组合如下:

 

例如如下调用:

<span style="font-size:18px;">int mysock;mysock = socket(AF_INET, SOCK_STREAM, 0);</span>


 

 表示创建一个流套接字, 底层通信协议是TCP.

再比如:

<span style="font-size:18px;">int mysock;mysock = socket(AF_UNIX, SOCK_DGRAM, 0); </span>

 

表示要创建一个用于同一台机器进程间通信的数据报套接字。

该函数失败返回-1, 并置errno。

socket()创建的只是一个套接字, 即两个通信端口之一。 如果通信的两个进程是有fork() 派生的具有共同祖先的进程, 可以使用socketpair创建一对套接字:

 

<span style="font-size:18px;">include <sys/socket.h>int socketpair(int domin, int type, int protocol, int filedes[2]);</span>


返回这对套接字于filedes[0]和filedes[1], 这一对套接字的通信是全双工的通信通道, 两端均可执行读写操作。

对于多数系统, domain指定为AF_UNIX, protocol设置为0.

socketpair() 通常用于父子进程通信。 一个描述字给父进程使用, 一个描述字给子进程使用。

父进程需要关闭子进程的描述字, 使得其不能使用子进程的描述字, 子进程需要关闭父进程的, 从而避免不用父进程的描述字, 程序如下:

 

#include <sys/socket.h>#include <stdio.h>#include <unistd.h>#include <errno.h>#include <stdlib.h> // for exit()#define DATA1 "Fine, thanks."#define DATA2 "hello, how are you?" </span><span style="font-size:18px;">#define err_exit(m) \  do {\perror(m); \exit(EXIT_FAILURE); \} while(0);int main() {int sockets[2], child;char buf[1024];if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) {err_exit("socketpair error");}if((child = fork()) == -1) { // 创建子进程err_exit("fork error");}if(child != 0) { // 这是父进程的代码close(sockets[0]); // 关掉子进程的套接字, 读来自子进程的消息if(read(sockets[1], buf, sizeof(buf)) < 0) { // 读buff中的消息</span><span style="font-size:18px;">err_exit("reading socket error");}printf("parent %d received request: %s\n", getpid(), buf);if(write(sockets[1], DATA1, sizeof(DATA1)) < 0) { // 向子进程写消息err_exit("writting socket error.");}close(sockets[1]); // 通信结束}else {close(sockets[1]); // 关闭父进程的套接字端if(write(sockets[0], DATA2, sizeof(DATA2)) < 0) { // 发送消息给父进程err_exit("writing socket error");}if(read(sockets[0], buf, sizeof(buf)) < 0) { //读来自父进程的消息,读到buf中err_exit("reading socket error");}printf("child process %d received answer: %s\n", getpid(), buf);close(sockets[0]); //通信结束}return 0;}



运行结果如下:



当不需要套接字的时候, 可以调用close()关闭它, 如同关闭一个文件一样。 关闭套接字之后, 套接字就不存在了。 有的时候, 不需要关闭套接字本身, 只需要断开连接, 此时调用int shutdown(int socket, int how)即可。

socket通信, 看谁先发起。 上述是子进程先写, 即先发起通信。

 

套接字的地址结构:

socket()创建套接字只是创建了本地系统的一个开放资源。 如果希望其他的进程能够与这个套接字通信, 该套接字必须有一个地址。

在UNIX通信域中, 路径名字就是套接字地址。

在Internet通信域中, 套接字地址是由主机IP地址加上端口号组成的。

 

点分十进制IP地址和二进制IP地址转换。

IPv4地址转换:

函数:下面的函数将点分十进制的字符串表示的IP地址转换成32位网络字节顺序。, 并将转换结果存在指针addr所指的in_addr结构体中。

#inlcude <arpa/inet.h>extern int inet_aton(const char *name, struct in_addr *addr);</span></span>

 

下面函数将32位字节顺序IP地址转换成点分十进制表示:

#inlcude <arpa/inet.h>extern char* inet_ntoa(struct in_addr addr);</span></span>

 

域名地址

网络中的一台计算机除了可以使用IP地址标识, 也可以用域名标识。 域名的采用层次结构方法命名的。

它有"."分隔的名字组成。 从左到右用“.”连接起来。 如www.google.comwww.baidu.com,www.hust.edu.cn等等 就是一个域名。每一个域名都对应一个IP地址。 位于最前面的计算机表示在主机在局域网中的主机名。 较后的名字则指明高一级的域名。     使用域名能够帮助人们记住服务程序所运行的这台计算机的名字。 域名地址都要转换为IP地址。 DNS(域名系统)就是实现主机的域名(主机名)到IP地址的映射一个分布式数据库。 处于局域网中的每一台计算机可以有多个域名。 他们是同一台主机的别名, 即指向同一个域名的指针。

下面举一个例子: 使用gethostbyname()和gethostbyaddr()来从(域名系统)数据库中获得一台主机的完整地址信息, 包括主机名字, 别名, 和IP地址。 即我们使用这两个函数做

函数如下:

#include <sys/socket.h>#include <netdb.h>struct hostent *gethostbyname(const char *name); // DEPRECATED! 不再使用了struct hostent *gethostbyaddr(const char *addr, int len, int type);



NONO, 都被deprecated了, 应该使用如下函数getaddrinfo和:这个函数将会返回关于一个特定Host name(即IP地址)的相关信息。 并将相关信息装入struct sockaddr, 完全替代了以前的gethostbyname以及getserverbyname的函数。 getaddrinfo不仅适用于IPv4, 也适用于IPv6. host name就存在参数nodename下面, 可以是“www.baidu.com”, 或者是IPv4或者IPv6的地址。servicename参数吃的是端口号,例如端口号80是用于HTTP 的, 相关的端口号位于/etc/services 文件中, 例如http, ftp, telnet, smtp等等等等。  

include <sys/types.h>#include <sys/socket.h>#include <netdb.h>int getaddrinfo(const char *nodename, const char *servname,                const struct addrinfo *hints, struct addrinfo **res);void freeaddrinfo(struct addrinfo *ai);const char *gai_strerror(int ecode);struct addrinfo {  int     ai_flags;          // AI_PASSIVE, AI_CANONNAME, ...  int     ai_family;         // AF_xxx  int     ai_socktype;       // SOCK_xxx  int     ai_protocol;       // 0 (auto) or IPPROTO_TCP, IPPROTO_UDP   socklen_t  ai_addrlen;     // length of ai_addr  char   *ai_canonname;      // canonical name for nodename  struct sockaddr  *ai_addr; // binary address  struct addrinfo  *ai_next; // next structure in linked list};
<span style="font-size:18px;">和getnameinfo: 这个函数的作用和上面的getaddrinfo()完全相反。 这个函数吃一个已经装入信息的sockaddr变量,然后查找一个name 或者service name。 这个函数完全替代了gethostbyaddr和getserverbyport。 </span>
include <sys/socket.h>#include <netdb.h>int getnameinfo(const struct sockaddr *sa, socklen_t salen,                char *host, size_t hostlen,                char *serv, size_t servlen, int flags);


举个例子如下:

/*** showip.c -- show IP addresses for a host given on the command line*/#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/socket.h>#include <netdb.h>#include <arpa/inet.h>#include <netinet/in.h>int main(int argc, char *argv[]){    struct addrinfo hints, *res, *p;    int status;    char ipstr[INET6_ADDRSTRLEN];    if (argc != 2) {        fprintf(stderr,"usage: showip hostname\n");        return 1;    }    memset(&hints, 0, sizeof hints);    hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version    hints.ai_socktype = SOCK_STREAM;    if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) {        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));        return 2;    }    printf("IP addresses for %s:\n\n", argv[1]);    for(p = res;p != NULL; p = p->ai_next) {        void *addr;        char *ipver;        // get the pointer to the address itself,        // different fields in IPv4 and IPv6:        if (p->ai_family == AF_INET) { // IPv4            struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr;            addr = &(ipv4->sin_addr);            ipver = "IPv4";        } else { // IPv6            struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr;            addr = &(ipv6->sin6_addr);            ipver = "IPv6";        }        // convert the IP to a string and print it:        inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr);        printf("  %s: %s\n", ipver, ipstr);    }    freeaddrinfo(res); // free the linked list    return 0;}


 

最后, 说说字节顺序(byte order)和大小端的问题。

网络通信使得数据从一个主机传递到另一个主机。 不同的处理器在在管理内存单元数据时, 对需要存放多个内存单元(一个byte视为一个内存单元)的一个数据的处理方式不同。

目前CPU数据管理主要有大端(big edian)和小端(little edian)两种模式。

小端模式操作数的存放顺序是高地址放高字节。

例如, 一个无符号16进制数0x12345678存放在0x4000到0x4003地址上, 存放格式如下:

0x4000 0x78

0x4001 0x56

0x4002 0x34

0x4003 0x12

大端模式是高地址放低字节:

0x4000 0x12

0x4001 0x34

0x4002 0x56

0x4003 0x78

测试程序:

 

#include <stdio.h>#include <netinet/in.h>int main(){   int i_num = 0x12345678;    printf("[0]:0x%x\n", *((char *)&i_num + 0)); // low address    printf("[1]:0x%x\n", *((char *)&i_num + 1));    printf("[2]:0x%x\n", *((char *)&i_num + 2));    printf("[3]:0x%x\n", *((char *)&i_num + 3));     i_num = htonl(i_num);    printf("[0]:0x%x\n", *((char *)&i_num + 0));    printf("[1]:0x%x\n", *((char *)&i_num + 1));    printf("[2]:0x%x\n", *((char *)&i_num + 2));    printf("[3]:0x%x\n", *((char *)&i_num + 3));    return 0;} 

在X64运行如下:

既然网络上传输的数据的字节顺序有差异, 所以在X86的平台下编写网络程序时要注意大小端的转换。 例如绑定socket端口和IP地址时都要使用网络字节顺序。
 目前, X86计算机主要支持小端模式, 而网络字节顺序支持大端模式。 有些处理器即支持小端模式, 有支持大端模式。

有如下四种抓换函数:

#include <netinet/in.h>uint16_t htons(uint16_t host16bitvalue); // short host to netuint32_t htonl(uint32_t host32bitvalue);  // long host to netuint16_t ntohs(uint16_t net16bitvalue); // short net to hostuint32_t ntohl(uint32_t net32bitvalue);   //long net to host

在存储主机信息的时候, IP地址和端口号均存储为网络字节顺序。

struct sockaddr_in s_addr;s_addr.sin_port = htons(7838);

(1)对于单字节数据, 不存在字节顺序问题, 直接发送。

char buf[] = "this is a test";...ret = send(socketfd, buf, strlen(buf), 0);


 

(2) 对于多字节顺序, 需要先先转换为多字节数据, 如short, int,等, 都必须首先转换为大端模式再发送。

如下:

int age = 30;...age = htonl(age);ret = send(socketfd, (void*)&age, sizeof(int), 0)

(3)对于结构体数据, 传送更为复杂。

例如如下:

struct member {    char name[32];    int age; // 需要转换    char gender;    char addr[128];};struct member personInfo;

把personInfo 发送给对方,可以采用以下办法:

(1)双方均知道member结构体的定义。 所有, 不管人名是几个字节, 不管这个人的住址是几个字节, 发送方可以按照如下方式发送:

struct member personInfo;....ret =send(socketfd, personInfo.name, 32, 0);personInfo.age = htonl(personInfo.age);ret =send(socketfd, (void*)&personInfo.age, sizeof(int), 0);...... ret =send(socketfd,  &personInfo.gender, sizeof(char), 0);....ret =send(socketfd, personInfo.addr, 128, 0);...


接收方如下接受:

struct member personInfo;....ret =recv(socketfd, personInfo.name, 32, 0);ret =recv(socketfd, (void*)&personInfo.age, sizeof(int), 0);personInfo.age = ntohl(personInfo.age);...... ret =send(socketfd,  &personInfo.gender, sizeof(char), 0);....ret =send(socketfd, personInfo.addr, 128, 0);...


另一种办法就是对数据进行pack处理。 发送方如下写程序:

#pack(1)struct member personInfo;........personInfo.age = htonl(personInfo.age);ret =send(socketfd, (void*)&personInfo, sizeof(member), 0);

接收方:

#pack(1) // 不一定是1, 关键是双方定义保持一致struct member personInfo;....ret =recv(socketfd, (void*)&personInfo, sizeof(struct member), 0);personInfo.age = ntohl(personInfo.age);

这一方式关键是对其方式定义相同

0 0
原创粉丝点击