网络编程1-cs模型的TCP通信

OSI七层模型

(物理层、数据链路层)网络链路层、网络层、传输层、(会话层、表示层、应用层)应用层

物理层:主要定义物理设备标准(网线的接口类型、光纤的接口类型、各种传输介质的传输速率等),用以传输比特流(涉及数模转换与模数转换)
数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址
网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择
传输层:端到端传输数据的基本功能;如 TCP、UDP。
会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。
表示层:数据格式标识,基本压缩加密功能。
应用层:

前话

协议:是数据传输和数据的解释的规则

数据在网络中传递需要进行封装。数据在四层模型中进入网络环境的传递过程(解封装时是逆的过程,最终露出数据)是:

数据-》应用层协议-》传输层协议-》网络层协议-》链路层协议  --- 网络环境

传输层、网络层、网络连接层封装由内核提供,应用层由用户进程提供(后面将介绍如何使用socketAPI编写应用程序)

不同的协议层对数据包有不同的称谓

    在传输层叫做段(segment)
    在网络层叫做数据报(datagram)
    在链路层叫做帧(frame)

数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。

典型协议

TCP传输层协议(TransmissionControl Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议(Hyper TextTransfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网互联协议(Internet Protocol)
ICMP协议是Internet控制报文协议(Internet ControlMessage Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group ManagementProtocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address ResolutionProtocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。

七层模型表示的就是OSI模型,四层模型表示的就是TCP/IP模型

OSI七层模型

物、数、网、传、会、表、应

物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

4层模型

应用层:http、ftp、nfs、ssh、telnet。。。
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
网络链路层:以太网帧协议、ARP

BS和CS模型

c/s模型:

client-server(客户端)

b/s模型:

browser-server(浏览器)

C/S B/S

优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小、速度快

缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http

安全性,比如说app软件安装需要下载在本地,有可能会窃取信息

各层协议

(链路层)以太网帧和ARP请求

网络是由一个又一个的路由器组成的,数据包进入网络实际上就是从一个路由器节点到另一个路由器节点,数据从一个路由器节点到另一个路由器节点借助的就是以太网帧协议

ARP协议:根据 Ip 地址获取 mac 地址。
以太网帧协议:根据mac地址,完成数据包传输。

以太网地址:mac地址

以太网目的地址(接收端以太网地址):ff:ff:ff:ff:ff:ff;
以太网源地址(发送端以太网地址):00:0c:29:a9:9c:e5
ARP请求:0806
发送端ip地址:192.168.1.108
目的地址:123.46.76.22

以上协议封装完毕,将其发送到一个路由节点

发送的如果是ARP请求的话,路由器收获到请求后如何获取到mac地址呢?
    在路由器当中有一个路由表,路由表中记录着其他和它所连接的IP地址,路由器在收到ARP请求后会发一个广播询问是谁的,而收到的路由器会比对IP地址,若不相符的话,收到的路由器会直接将ARP请求抛弃,相符的路由器就做ARP应答

(网络层)IP协议

IP数据报格式:

版本: IPv4、IPv6  -- 4位

    TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃

    源IP: 32位。--- 4字节        192.168.1.108 --- 点分十进制 IP地址(string)  --- 真正进入到网络中,在计算机中传输的是二进制 

    目的IP:32位。--- 4字节

(传输层)端口号和UDP、TCP协议

MAC地址和IP地址的区别与联系(计算机网络篇)_gogo_hua的博客-CSDN博客_mac地址和ip地址

Mac地址:网卡的编号(全球唯一)(物理地址)
ip地址:可以在网络环境中,唯一标识一台主机
端口号:可以在网络的一台主机上,唯一标识一个进程
ip地址+端口号:在网络环境中唯一标识一个进程

IP地址在IP协议中;端口号在UDP协议和TCP协议中
8080端口,http协议常用端口;绝大数协议5000以下,5000以上

UDP协议:

TCP协议:

CS模型的TCP通信

预备知识

套接字

Socket在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。可以使用文件描述符引用套接字。

与管道类似的,Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递

在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

套接字通讯原理示意

在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。使用同一个文件描述符索发送缓冲区和接收缓冲区

网络字节序

数据传输时如何定义网络数据流的地址?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

htonl --> 本地--》网络 (主要针对IP协议)            192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
192.168.1.11是string类型,借助atoi转换成int类型,从而输入到htonl函数中

htons --> 本地--》网络 (主要针对port)  端口
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port) 端口

h表示host,n表示network,l表示32位长整数,s表示16位短整数。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

IP地址转换函数

上面函数不好用,给你封装函数

int inet_pton(int af, const char *src, void *dst);  本地字节序(string IP) ---> 网络字节序
       af:AF_INET、AF_INET6
       src:传入,IP地址(点分十进制)
       dst:传出,转换后的网络字节序的 IP地址。 

       返回值:
           成功: 1
           异常: 0, 说明src指向的不是一个有效的ip地址。
           失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);  网络字节序---> 本地字节序(string IP)

       af:AF_INET、AF_INET6
       src:网络字节序IP地址
       dst:本地字节序(string IP)
       size: dst 的大小。
 
       返回值:成功:dst。    
              失败:NULL

sockaddr地址结构

trcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型

sockaddr数据结构

sockaddr_in原型

struct sockaddr_in {
    __kernel_sa_family_t sin_family;       /* Address family */      地址结构类型
    __be16 sin_port;                      /* Port number */        端口号
    struct in_addr sin_addr;              /* Internet address */    IP地址

struct in_addr {                        /* Internet address. */
    __be32 s_addr;
};
    //bind函数使用sockaddr
    struct sockaddr_in addr;

    addr.sin_family = AF_INET/AF_INET6                man 7 ip

    addr.sin_port = htons(9527);
            
 //       int dst;

 //       inet_pton(AF_INET, "192.157.22.45", (void *)&dst);

 //       addr.sin_addr.s_addr = dst;

    【*】addr.sin_addr.s_addr = htonl(INADDR_ANY);        取出系统中有效的任意IP地址。二进制类型。

    bind(fd, (struct sockaddr *)&addr, size);//有bind函数时还没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前要强制类型转换一下

各函数原型

socket、bind、listen、accept、connect函数

socket函数:

    #include <sys/socket.h>

    int socket(int domain, int type, int protocol);        创建一个 套接字

        domain:AF_INET、AF_INET6、AF_UNIX

        type:SOCK_STREAM(流式协议)、SOCK_DGRAM(报式协议)

        protocol: 0 (SOCK_STREAM:TCP; SOCK_DGRAM:UDP)

        返回值:
            成功: 新套接字所对应文件描述符
            失败: -1 errno

bind函数:
    #include <arpa/inet.h>

     int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);     给socket绑定一个 地址结构 (IP+port)

        sockfd: socket 函数返回值

        struct sockaddr_in addr;

            addr.sin_family = AF_INET;

            addr.sin_port = htons(8888);

            addr.sin_addr.s_addr = htonl(INADDR_ANY);

            addr: 传入参数(struct sockaddr *)&addr

        addrlen: sizeof(addr) 地址结构的大小。

        返回值:

            成功:0

            失败:-1 errno
listen函数:
    int listen(int sockfd, int backlog);    设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)

        sockfd: socket 函数返回值

        backlog:上限数值。最大值 128.

        返回值:
            成功:0
            失败:-1 errno
accept函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);    阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。

        sockfd: socket 函数返回值

        addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)
            socklen_t clit_addr_len = sizeof(addr);

        addrlen:传入传出。 &clit_addr_len
             入:addr的大小。 出:客户端addr实际大小。
        返回值:
            成功:能与客户端进行数据通信的 socket 对应的文件描述符。
            失败: -1 , errno
connet函数:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);      使用现有的 socket 与服务器建立连接

        sockfd: socket 函数返回值

        struct sockaddr_in srv_addr;        // 服务器地址结构

            srv_addr.sin_family = AF_INET;

            srv_addr.sin_port = 9527     跟服务器bind时设定的 port 完全一致。

            inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);

        addr:传入参数。服务器的地址结构

        addrlen:服务器的地址结构的大小

        返回值:
            成功:0
            失败:-1 errno
  如果不使用bind绑定客户端地址结构, 采用"隐式绑定".

创建CS模型的TCP通信

客户端要创建一个套接字,服务端要创建一个套接字,用以通信;还要创建一个用于监听的套接字

所以是3个套接字

进行大小写转换(客户端写入小写字母,服务器端进行转换成大写,之后传到客户端进行显示)
服务器端

1、使用socket创建套接字,会有一个文件描述符索引fd(句柄)--唯一入口用以操作套接字
2、bind函数用以向套接字绑定ip地址和端口
3、listen函数用来设置监听上限(同时建立连接的数量)
4、accept函数阻塞监听客户端连接,其需要用到一个socket作为参数传入;当accept成功和客户端建议连接以后,会返回一个新的socket;
        1中建立的socket用于监听,等待其他连接;
        返回的socket用于与客户端建立连接;
5. read(fd)    读socket获取客户端传过来的数据数据
6. 小--大写    toupper()
7. write(fd)
8. close();

客户端

1、使用socket创建套接字,会有一个文件描述符索引fd(句柄)--用以操作套接字
2、使用connect指定用来跟客户端socket用来连接的ip地址和端口号
Connect后就和服务端建立连接了

3. write()    写数据到 socket,一写到socket就会自然而然写到服务器中
4. read()    读转换后的数据。
5. 显示读取结果
6. close()

server的实现

#include <stdio.h>  
#include <ctype.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <errno.h>  
#include <pthread.h>  
  
#define SERV_PORT 9527  
  
  
void sys_err(const char *str)  
{  
    perror(str);  
    exit(1);  
}  
  
int main(int argc, char *argv[])  
{  
    int lfd = 0, cfd = 0(返回的新的描述符);  
    int ret, i;  
    char buf[BUFSIZ], client_IP[1024];  
  
    struct sockaddr_in serv_addr, clit_addr;  // 定义服务器地址结构 和 客户端地址结构  
    socklen_t clit_addr_len;                  // 客户端地址结构大小  
  
    serv_addr.sin_family = AF_INET;             // IPv4  
    serv_addr.sin_port = htons(SERV_PORT);      // 转为网络字节序的 端口号  
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 获取本机任意有效IP  
  
    lfd = socket(AF_INET, SOCK_STREAM, 0);      //创建一个 socket  
    if (lfd == -1) {  
        sys_err("socket error");  
    }  
  
    bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)  
  
    listen(lfd, 128);                   //  设置监听上限  
  
    clit_addr_len = sizeof(clit_addr);  //  获取客户端地址结构大小  
  
    cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);   // 阻塞等待客户端连接请求  
    if (cfd == -1)  
        sys_err("accept error");  
  
    printf("client ip:%s port:%d\n",   
            inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),   
            ntohs(clit_addr.sin_port));         // 根据accept传出参数,获取客户端 ip 和 port  
  
    while (1) {  
        ret = read(cfd, buf, sizeof(buf));      // 读客户端数据  
        write(STDOUT_FILENO, buf, ret);         // 写到屏幕查看  
  
        for (i = 0; i < ret; i++)                // 小写 -- 大写  
            buf[i] = toupper(buf[i]);  
  
        write(cfd, buf, ret);                   // 将大写,写回给客户端。  
    }  
  
    close(lfd);  
    close(cfd);  
  
    return 0;  
}    
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
accept函数中的clit_addr传出的就是客户端地址结构,IP+port

于是,在代码中增加此段代码,可获取客户端信息:
printf("client ip:%s port:%d\n", 
            inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)), 
            ntohs(clit_addr.sin_port));

client的实现

#include <stdio.h>  
#include <sys/socket.h>  
#include <arpa/inet.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <errno.h>  
#include <pthread.h>  
  
#define SERV_PORT 9527  
  
void sys_err(const char *str)  
{  
    perror(str);  
    exit(1);  
}  
  
int main(int argc, char *argv[])  
{  
    int cfd;  
    int conter = 10;  
    char buf[BUFSIZ];  
      
    struct sockaddr_in serv_addr;          //服务器地址结构  
  
    serv_addr.sin_family = AF_INET;  
    serv_addr.sin_port = htons(SERV_PORT);  
    //inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);  
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);  
  
    cfd = socket(AF_INET, SOCK_STREAM, 0);  
    if (cfd == -1)  
        sys_err("socket error");  
  
    int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));  
    if (ret != 0)  
        sys_err("connect err");  
  
    while (--conter) {  
        write(cfd, "hello\n", 6);  
        ret = read(cfd, buf, sizeof(buf));  
        write(STDOUT_FILENO, buf, ret);  
        sleep(1);  
    }  
  
    close(cfd);  
  
    return 0;  
}   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值