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网络编程系列之三(初级)的更多相关文章

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

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

  2. TCP/IP网络编程系列之一(初级)

    概述 网络编程实际上就是编写程序使两台联网的计算机相互的交换数据.操作系统会提供名为“ 套接字 ”的部件.套接字是网络数据传输的软件设备,即使对网络数据传输原理不太熟悉也无关紧要.我们也能通过套接字完 ...

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

    套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,doma ...

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

       手把手叫你玩转网络编程系列之三    完毕port(Completion Port)具体解释                                                    ...

  5. TCP/IP网络编程之多进程服务端(二)

    信号处理 本章接上一章TCP/IP网络编程之多进程服务端(一),在上一章中,我们介绍了进程的创建和销毁,以及如何销毁僵尸进程.前面我们讲过,waitpid是非阻塞等待子进程销毁的函数,但有一个不好的缺 ...

  6. 浅谈TCP/IP网络编程中socket的行为

    我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...

  7. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  8. TCP/IP网络编程之多线程服务端的实现(二)

    线程存在的问题和临界区 上一章TCP/IP网络编程之多线程服务端的实现(一)的thread4.c中,我们发现多线程对同一变量进行加减,最后的结果居然不是我们预料之内的.其实,如果多执行几次程序,会发现 ...

  9. TCP/IP网络编程之优于select的epoll(二)

    基于epoll的回声服务端 在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例. echo_epollserv ...

随机推荐

  1. String.Remove

    String.Remove方法注意事项: 1.该方法不改变元字符串: 2.str  = ‘’: str.Remove(str.Length-3);明显超限,但是不报错,返回值为''; str = '1 ...

  2. python爬虫之requests模块

    一. 登录事例 a. 查找汽车之家新闻 标题 链接 图片写入本地 import requests from bs4 import BeautifulSoup import uuid response ...

  3. LeetCode OJ:Peeking Iterator(peeking 迭代器)

    Given an Iterator class interface with methods: next() and hasNext(), design and implement a Peeking ...

  4. (转)MapReduce Design Patterns(chapter 6 (part 2))(十二)

    Chain Folding 这是对job 链的一种优化.基本上是一种大体规则:每条记录都会提交给多个mapper,或者给reducer然后给mapper.这种综合处理方法会节省很多读文件和传输数据的时 ...

  5. paddlepaddle初步印象

    从其官网整理了一些资料如下: 1.基本概念 基本使用概念 PaddlePaddle是源于百度的一个深度学习平台.PaddlePaddle为深度学习研究人员提供了丰富的API,可以轻松地完成神经网络配置 ...

  6. 基于Vue的后台选择推荐

    引言: Vue.js目前是业界大名鼎鼎的Web解决方案,具体有点,我这里就不再赘述了,感兴趣的童鞋自行查找阅读,这里罗列一下,这几天自己研究的成果,管理后台. 管理后台 Vue Element Adm ...

  7. 在ROS Kinetic中使用Gazebo 8进行机器人仿真

    在ROS Kinetic中使用Gazebo 8比在ROS Indigo中使用Gazebo 3-8要容易一些. 目前最新稳定版本的Gazebo8为8.1.1. 安装流程如下: $  sudo apt-g ...

  8. [置顶] 云端TensorFlow读取数据IO的高效方式

    低效的IO方式 最近通过观察PAI平台上TensoFlow用户的运行情况,发现大家在数据IO这方面还是有比较大的困惑,主要是因为很多同学没有很好的理解本地执行TensorFlow代码和分布式云端执行T ...

  9. IOS开发 警告 All interface orientations must be supported unless the app requires full screen.

    在IOS开发中遇到警告  All interface orientations must be supported unless the app requires full screen. 只要勾上R ...

  10. 实战maven私有仓库三部曲之一:搭建和使用

    在局域网内搭建maven私有仓库,可避免每次都从中央仓库下载公共jar包,另外将A模块作为二方库发布到私有仓库后,B模块可以很方便的引用,今天我们就来实战maven私有仓库的搭建和使用: 原文地址:h ...