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

}

运行结果如下图所示:

时间: 2024-10-05 10:28:36

TCP/IP网络编程系列之三(初级)的相关文章

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指主机

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

TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的套接字.在了解TCP之前,先了解一下TCP所属的TCP/IP协议栈. 如图所示,TCP/IP协议栈共分为4层,可以理解成数据收发分成了4个层次化过程. 链路层 它是物理链接领域标准化结果,也是最基本的领域,专门定义LAN.WAN.MAN等网络标准.若两台计算机通过网络进行数据交换,链路层就负责整个物

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

套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,domain是套接字使用中的协议族(Protocol Family)信息.type套接字类型里面的数据传输类型信息.protocol计算机通信中使用的协议信息. 协议族(Protocol Family) 协议族类型有: PE_INET IPV4 PE_INET6 IPV6 PF_LOCAL 本地通信的UNI

TCP/IP网络编程系列之二

套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,domain是套接字使用中的协议族(Protocol Family)信息.type套接字类型里面的数据传输类型信息.protocol计算机通信中使用的协议信息. 协议族(Protocol Family) 协议族类型有: PE_INET IPV4 PE_INET6 IPV6 PF_LOCAL 本地通信的UNI

TCP/IP网络编程系列之一

概述 网络编程实际上就是编写程序使两台联网的计算机相互的交换数据.操作系统会提供名为“ 套接字 ”的部件.套接字是网络数据传输的软件设备,即使对网络数据传输原理不太熟悉也无关紧要.我们也能通过套接字完成数据传输,因此网络编程又叫套接字编程. 过程  我们可以把套接字理解为我们平时的电话机,我们先看一下套接字的创建过程: 首先你如果要和别人沟通肯定要安装好电话机才可以,所以对应套接字的是调用socket函数时进行对话. #include<sys/socket.h> int socket(int

完毕port(CompletionPort)具体解释 - 手把手教你玩转网络编程系列之三

手把手叫你玩转网络编程系列之三    完毕port(Completion Port)具体解释                                                              ----- By PiggyXP(小猪) 前 言 本系列里完毕port的代码在两年前就已经写好了,可是因为许久没有写东西了,不知该怎样提笔,所以这篇文档总是在酝酿之中--酝酿了两年之后,最终决定開始动笔了,但愿还不算晚-.. 这篇文档我很具体而且图文并茂的介绍了关于网络编程模型中完毕

TCP/IP 网络编程 (抄书笔记 1) -- TCP

TCP/IP 网络编程 (抄书笔记 1) – TCP TCP/IP 网络编程 (抄书笔记 1) – TCP Table of Contents server client 更好的 client 端实现 来源: <TCP/IP 网络编程> 抄书: 通信的双方都各自 拥有 输入缓存和输出缓存 socket 的 write 函数并不是立即传输数据, 而是写到输出缓存区, 到达另一端的输入缓存区 socket 的 read 函数调用的瞬间, 就从输入缓存区中读取数据 TCP 协议中的滑动窗口会保证 数

《TCP/IP网络编程》

<TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6-19 出版日期:2014 年6月 开本:16开 页码:1 版次:1-1 所属分类:计算机 > 计算机网络 > 网络协议 > TCP/IP 更多关于>>><TCP/IP网络编程> 编辑推荐 为初学者准备的网络编程 本书涵盖操作系统.系统编程.TCP/IP协议等多种

TCP/IP 网络编程 (抄书笔记 2) -- UDP

TCP/IP 网络编程 (抄书笔记 2) – UDP TCP/IP 网络编程 (抄书笔记 2) – UDP Table of Contents server client connect 来源: <TCP/IP 网络编程> 抄书: TCP 协议若要向 10 个客户端提供服务, 除了需要 listen 套接字外, 还需要 10 个服务器端套接字 (accept), 但是在 UDP 中, 不管是服务器端还是客户端都只需要 1 个套接字 udp 的 client 不需要 bind, 调用 sendt