TCP/IP网络编程系列之三(初级)

Posted 天狼鼠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP网络编程系列之三(初级)相关的知识,希望对你有一定的参考价值。

TCP/IP网络编程系列之三-地址族与数据序列

分配给套接字的IP地址和端口

  IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值。端口号并非赋予计算机值,而是为了区分程序中创建的套接字而分配给套接字的序号。

网络地址

  网络地址分为IPV4和IPV6,分别你别为4个字节地址簇和6个字节地址簇。IPV4标准的4个字节的地址分为网络地址和主机地址,且分为A、B、C、D、E 等类型。一般很少用到E类型。如下图所示:net-id指网络ID,host-id指主机ID

 

说明:

A类地址的首字节范围是:0-127

B类地址的首字节范围是:128-191

C类地址的首字节范围是:192-223

或者也可以这样分

A类地址的首位是以0开头

B类地址的前两位是以10开头

C类地址的前三位是以110开头

概述

网络ID是为区分网络而设置的一部分IP地址。比如你向www.baidu.com公司传输数据,该公司内部构建了局域网,把所有的计算机连接起来。因此,首相向baidu.com网络传输数据,也就是说,并非一开始就浏览所有4字节的IP地址,进而找到目标主机;而是仅浏览4字节IP地址的网络地址,先把数据传到baidu.com的网络。baidu.com网络接收到数据之后,浏览传输数据的主机地址并将数据传输给目标地址。一般的网络都会有路由器和交换机,所以实际上是向路由器或交换机传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传送数据。

用于区分套接字的端口号

IP用于区分计算机,只要有IP地址就能想目标主机传输数据,但是仅凭这些数据无法传输给最终的应用程序。假设在欣赏音乐的同时在听音乐或者上网浏览网页,这时至少需要1个接受视频数据的套接字和1个接受网页信息的套接字。但是如何区分它们呢,也就是说传输到计算机的网络数据是发送给视频播放器还是音乐播放器?假设我们开发了迅雷等应用程序,该程序用块单位分割一个文件,从多台计算机接受数据。那如何区分这些套接字呢?

计算机中一般都会有NIC(NetWork Interface Card,网络接口卡)数据传输设备。通过NIC向计算机内部传输数据时会用到IP。OS负责把传递到内部的数据适当的分配给套接字,这时就要利用端口号。通过NIC接受的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字。端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将一个端口号分配给不同的套接字。并且,端口号由16位构成,可分配的端口号的范围是0-65535,0-1023是知名端口号(Well-Know PORT),一般分配给特定应用程序,所以应当分配此范围之外的值。端口号是不能重复,但TCP套接字和UDP套接字不会公用端口号,所以允许重复。比如:某TCP套接字使用9190端口号,则其他TCP无法就无法使用端口号,但是UDP套接字就可以使用。

总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最总的目的应用程序(应用程序套接字)。

地址信息的表示

应用程序中使用的IP地址和端口号以结构体的形式给出了定义,我们主要以IPV4为中心。

复制代码
struct sockaddr_in
{
      sa_family_t          sin_family;//地址族
      uint16_t              sin_port;//16位TCP/UDP端口号
      struct in_addr      sin_addr;//32位IP地址
      char                    sin_zero[8];//不使用
};
该结构体中的 in_addr用来存放32位的IP地址,定义如下
struct in_addr
{
    In_addr_t    s_addr;//32位IP地址
};
复制代码

uint16_t in_addr_t等类型可以参考POSIX,我这边简单说一下

POSIX中定义的数据类型
数据类型 数据类型说明 声明的头文件
int8_t signed 8-bit int sys/types.h
uint8_t unsigned 8-bit int  sys/types.h
int16_t signed 16-bit int sys/types.h
uint16_t unsigned 16-bit int  sys/types.h
int32_t signed  32-bit int 

sys/types.h

这主要是考虑到扩展性,如果使用int32_t类型的数据,就能保证在任何时候都占用4字节,即使将来用64位的来存储也是一样。

结构体sockaddr_in的成员分析

  • 成员sin_family

    每种协议族适用的地址族不同,比如IPV4使用4个字节地址族,IPV6使用16字节地址簇

      地址簇        含义

      AF_INET        IPV4网络协议中使用的地址族

      AF_INET6      IPV6网络协议中使用的地址族

      AF_LOCAL       本地通信中采用的UNIX协议的地址族

  • 成员sin_port

    该成员保存16位端口号,具体在下面讲解。

  • sin_addr

    保存32位的IP地址信息,且以网络字节序保存,结构体in_addr声明为uint32_t,一次只需要保存32位整数类型即可。

  • sin_zero

    无特殊含义,只是未使用结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员,必须填充为0,否则无法得到想要的结果。之前在服务端bind函数的时候,

struct sockaddr_in serv_addr;

if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr)==-1),其中第二个参数,是由sockaddr_in结构体转化而来的,并且希望得到sockaddr结构体变量地址,包括地址簇、端口号、IP地址等。但是我们直接向sockaddr结构体填充这些信息会带来麻烦。

struct sockaddr

{

  sa_family_t   sin_family;//地址族

  char       sa_data[14];//地址信息

};

sa_data保存地址信息,包括IP地址和端口号,剩余部分应填充为0,这也是bind函数的要求。这对于包含地址信息来讲非常麻烦,从而有了新的结构体sockaddr_in。

 网络字节序和地址转换

  • 字节序和网络字节序

cpu向内存保存数据有两种,这意味着cpu解析数据的方式也是两种分别为大端序和小端序。

  1. 大端序(Big Endian):高位字节存放到低位地址。
  2. 小端序(Little Endian):高位字节存放到高位地址。

比如0x00000001
大端序:内存低比特位 00000000 00000000 00000000 00000001 内存高比特位
小端序:内存低比特位 10000000 00000000 00000000 00000000 内存高比特位

还可以如下图表示:

所以出现了一个问题如果两台计算机的cpu的数据保存方式不同,但是他们是如何传送数据的呢?如何进行网络传输的呢?所以就规定了一个标准,在通过网络传输的过程中统一按照大端序。即先把数据数组转化为大端序格式然后进行传输。因此,所有计算机接受数据时应识别该数据的字节格式,小端序系统传输数据时应该转化为大端字节序的排列方式。

  • 字节序转换(Endian Conversions)

所以我们懂应该知道为何在填充sockadr_in结构提前将数据转化为网络字节序。转换字节的函数:

unsigned short htons (unsigned short);

unsigned  short ntohs(unsigned  short);

unsigned  long htons (unsigned long);

unsigned  long ntohs (unsigned long);

通过函数名可以知道他的功能,只要了解一下细节。htons中的h代表主机(host)字节序,n代表网络字节序。s指的是short,l指的是long(linux 中long类型占用4个字节),可以解释为"把short类型数据从主机字节序转化为网络字节序"。所以ntohs也就知道了吧。我们通过例子来看一下效果:

运行结果之后会看到如下图结果:

这就是小端序cpu运行结果,如果在大端值中是不会发生变化的。Intel和AMD系列的cpu都采用小端序标准。数据在传输过程中需要经过转换吗?实际上没有必要,这个过程是自动的。除了想sockaddr_in结构体变量填充数据外,其他情况不需要考虑字节序问题。

网络地址的初始化与分配

sockaddr_in 中保存地址信息的成员为32位整数型。因此,为了分配IP地址将其转化为32位整数型数据。对我们而言并非易事。对于IP地址的的表示,我们熟悉的是点分十进制,而非整型数据表示法。幸运的是,有个函数可以帮我们将字符串形式的IP地址转化为32位的整型数据。此函数在转换类型的同时进行网络字节序转换。

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string)
成功时返回32位大端序整型数据,失败时返回INADDR_NONE

下面是测试代码:

运行结果如下图所示:从运行结果可以看出,inet_addr函数不仅可以把ip地址转换为32位整数,而且还可以检测无效的ip地址。并且输出的确实是网络字节序。还有一个函数与inet_addr函数功能完全相同,只不过该函数利用了in_addr结构体,且使用频率更高。

#include <arpa/inet.h>

int inet_aton(const char *string,struct in_addr *addr)
成功时返回1,失败时返回0;

实际编程中若要调用inet_addr函数,需要将转化后的IP地址信息代入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数不需要此过程。原因在于,若传递in_addr结构体变量地址值,函数会自动把结果填入该结构体变量。ok,下面再讲解一个把网络字节序整数型IP地址转换成我们熟悉的字符串形式。

#include <arpa/inet>

char *inet_ntoa(struct in_addr adr);
成功时返回转换的字符串地址,失败时返回-1。

但在调用时小心,返回值是char类型的指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完函数后,应该立即将字符串信息复制到其他内存空间。因为在此调用该函数,则有可能覆盖之前保存的字符串信息。总之,再次调用该函数前返回的字符串地址值是有效的。如要长期保存,则应将字符串复制到其他内存空间。示例:

运行结果如下:

下面我把之前的代码完全重新组合一下。

服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void ErrorMessage(char *message);

int main(int argc,char *argv[])
{
    int serv_sock;
    int client_sock;
    struct sockaddr_in serv_addr;
    struct sockaddr_in client_addr;
    char *serverIP= "127.0.0.1";
    char *servPort = "9190";
    char message[]="Hi,TCPIP";
    socklen_t clnt_addr_size;
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    if(serv_sock==-1)
    {
        ErrorMessage("Sock Error!");
    }
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(serverIP);
    serv_addr.sin_port = htons(atoi(servPort));
    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    {
        ErrorMessage("Bind() Error");
    }
    if(listen(serv_sock,5)==-1)
    {
        ErrorMessage("listen() error");
    }
    clnt_addr_size = sizeof(client_addr);
    client_sock = accept(serv_sock,(struct sockaddr*)&client_addr,&clnt_addr_size);
    if(client_sock==-1)
    {
        ErrorMessage("accept() error");
    }
    write(client_sock,message,sizeof(message));
    close(client_sock);
    close(serv_sock);
    return 0;
}
void ErrorMessage(char *message)
{
    fputs(message,stderr);
    fputc(\'\\n\',stderr);
    exit(1);
}

客户端代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void ErrorMessage(char *message);

int main(int argc,char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    char *serv_port = "9190";
    int str_len;
    sock = socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1)
    {
        ErrorMessage("socket() error");
    }
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(serv_port));
    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    {
        ErrorMessage("connect() error");
    }
    str_len = read(sock,message,sizeof(message)-1);
    if(str_len==-1)
    {
        ErrorMessage("read() error");
    }
    printf("Message from server:%s\\n",message);
    close(sock);
    return 0;
}
void ErrorMessage(char *message)
{
    fputs(message,stderr);
    fputc(\'\\n\',stderr);
    exit(1);

}

运行结果如下图所示:

 

以上是关于TCP/IP网络编程系列之三(初级)的主要内容,如果未能解决你的问题,请参考以下文章

TCP/IP之三次握手

Linux TCP/IP协议之三次握手

初级程序员软考重点软件重点9 网络基础

初识Linux

网络编程

自动化运维Python系列之Socket编程