网络学习(二)--socket介绍

目录

一、引言
二、socket介绍
------> 2.1、socket
------> 2.2、socket类型
------> 2.3、基于网络模型的socket
------> 2.4、IP、MAC和端口号
------> 2.5、socket通讯流程详解
------> 2.6、通讯总结
------> 2.7、demo

一、引言

前一章介绍了网络模型,这章来介绍一下socket编程
原文链接 socket教程

二、socket介绍

1、socket

在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作,网络也是一样

我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
用 read() 读取从远程计算机传来的数据;
用 write() 向远程计算机写入数据。

2、socket类型

这个世界上有很多种套接字(socket),比如 DARPA Internet 地址(Internet 套接字)、本地节点的路径名(Unix套接字)、CCITT X.25地址(X.25 套接字)等。我们本章讲的都是第一种:Internet 套接字,它是最具代表性的,也是最经典最常用的。

我之前做过一个项目,就是通过socket完成多进程的通信,使用的就是Unix套接字

根据数据的传输方式,可以将 Internet 套接字分成两种类型。通过 socket() 函数创建连接时,必须告诉它使用哪种数据传输方式。

流格式套接字(SOCK_STREAM)

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

SOCK_STREAM 有以下几个特征:
数据在传输过程中不会消失;
数据是按照顺序传输的;
数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。

可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

为什么流格式套接字可以达到高质量的数据传输呢?这是因为它使用了 TCP 协议(The Transmission Control Protocol,传输控制协议),TCP 协议会控制你的数据按照顺序到达并且没有错误。

你也许见过 TCP,是因为你经常听说“TCP/IP”。TCP 用来确保数据的正确性,IP(Internet Protocol,网络协议)用来控制数据如何从源头到达目的地,也就是常说的“路由”。

那么,“数据的发送和接收不同步”该如何理解呢?
流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。

流格式套接字有什么实际的应用场景吗?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。

数据报格式套接字(SOCK_DGRAM)

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。

计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。

因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。

可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:
强调快速传输而非传输顺序;
传输的数据可能丢失也可能损毁;
限制每次传输的数据大小;
数据的发送和接收是同步的(有的教程也称“存在数据边界”)。

总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

3、基于网络模型的socket

网络模型究竟是干什么呢?简而言之就是进行数据封装的。

我们平常使用的程序(或者说软件)一般都是通过应用层来访问网络的,程序产生的数据会一层一层地往下传输,直到最后的网络接口层,就通过网线发送到互联网上去了。数据每往下走一层,就会被这一层的协议增加一层包装,等到发送到互联网上时,已经比原始数据多了四层包装。整个数据封装的过程就像俄罗斯套娃。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

给数据加包装的过程,实际上就是在数据的头部增加一个标志(一个数据块),表示数据经过了这一层,我已经处理过了。给数据拆包装的过程正好相反,就是去掉数据头部的标志,让它逐渐现出原形。

你看,在互联网上传输一份数据是多么地复杂啊,而我们却感受不到,这就是网络模型的厉害之处。我们只需要在代码中调用一个函数,就能让下面的所有网络层为我们工作。

我们所说的 socket 编程,是站在传输层的基础上,所以可以使用 TCP/UDP 协议,但是不能干「访问网页」这样的事情,因为访问网页所需要的 http 协议位于应用层。

那如何使用socket进行http通信呢,可以看这篇文章 如何使用socket进行Http请求和解析响应
其实就是在创建socket的时候指定通信接口,如 https的默认端口为443,http默认端口为80
两台计算机进行通信时,必须遵守以下原则:

必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。
每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。
数据只能逐层传输,不能跃层。
每一层可以使用下层提供的服务,并向上层提供服务。
4、IP、MAC和端口号

我们之前说,IP地址由网络地址和主机地址,根据掩码决定。

其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。

IP地址和MAC地址寻址协议层上的区别

1、MAC地址应用在OSI第二层,即数据链路层。数据链路层协议可以使数据从一个节点传递到相同链路的另一个节点上(通过MAC地址)。
2、IP地址应用于OSI第三层,即网络层。网络层协议使数据可以从一个网络传递到另一个网络上(ARP根据目的IP地址,找到中间节点的MAC地址,通过中间节点传送,从而最终到达目的网络)。

也就是说通过IP地址中的网络号和主机号找到具体的某台主机,指定通讯对象,这是在网络层实现,而但在网络层指定目标地址后,链路层如何找到这台计算机呢,就是通过MAC地址

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。如下在这里插入图片描述
物理地址是数据链路层和物理层使用的地址;IP地址是网络层及其以上层使用的地址。
具体可以看这篇文章计算机网络中MAC地址与IP地址

端口号

有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:
在这里插入图片描述

5、socket通讯流程详解

流程简单分析一下

1、socket:

创建一个socket描述符,参数需要传入协议域、socket类型、协议
其中协议域由以下几种完成

AF_UNIX(本机通信)
AF_INET(TCP/IP – IPv4)
AF_INET6(TCP/IP – IPv6)
其中 “type”参数指的是套接字类型,常用的类型有:
SOCK_STREAM(TCP流)
SOCK_DGRAM(UDP数据报)
SOCK_RAW(原始套接字)
2、bind:

把一个地址族中的特定地址赋给socket,其中需要指定IP、端口号
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;
而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

3、listen()、connect()

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

4、accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。
TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。
之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;

accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

5、read()、write()等函数

万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
• read()/write()
• recv()/send()
• readv()/writev()
• recvmsg()/sendmsg()
• recvfrom()/sendto()

我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。

6、close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

6、通讯总结
1、服务器端

首先服务器应用程序用系统调用socket来创建一个套接安,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。
接下来,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
然后,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,

这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接。

2、客户端

基于socket的客户端比服务器端简单,同样,客户应用程序首先调用socket来创建一个未命名的套接字,

然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信。

7、demo

服务器

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
 
 
#define MAX_MSG_SIZE 256
#define SERVER_PORT  9987
 
 
#define BACKLOG 2
 
 
int GetServerAddr(char * addrname){
    printf("please input server addr:");
    scanf("%s",addrname);
    return 1; 
}
 
 
int main(){  
    int sock_fd,client_fd; /*sock_fd:监听socket;client_fd:数据传输socket */  
    struct sockaddr_in ser_addr; /* 本机地址信息 */  
    struct sockaddr_in cli_addr; /* 客户端地址信息 */  
    char msg[MAX_MSG_SIZE];/* 缓冲区*/  
    int ser_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */  
    if(ser_sockfd<0)  
           {/*创建失败 */  
                  fprintf(stderr,"socker Error:%s\n",strerror(errno));  
                  exit(1);  
          }  
    /* 初始化服务器地址*/  
    socklen_t  addrlen=sizeof(struct sockaddr_in);  
    bzero(&ser_addr,addrlen);  
    ser_addr.sin_family=AF_INET;  
    ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
    ser_addr.sin_port=htons(SERVER_PORT);  
    if(bind(ser_sockfd,(struct sockaddr*)&ser_addr,sizeof(struct sockaddr_in))<0){ /*绑定失败 */  
            fprintf(stderr,"Bind Error:%s\n",strerror(errno));  
            exit(1);  
    }  
    /*侦听客户端请求*/  
    if(listen(ser_sockfd,BACKLOG)<0){  
        fprintf(stderr,"Listen Error:%s\n",strerror(errno));  
        close(ser_sockfd);  
        exit(1);  
    }  
    while(1){/* 等待接收客户连接请求*/  
        int cli_sockfd=accept(ser_sockfd,(struct sockaddr*) &cli_addr, &addrlen);  
        if(cli_sockfd<=0){  
            fprintf(stderr,"Accept Error:%s\n",strerror(errno));  
        }else{/*开始服务*/  
            recv(cli_sockfd, msg, (size_t)MAX_MSG_SIZE, 0); /* 接受数据*/  
            printf("received a connection from %sn", inet_ntoa(cli_addr.sin_addr));  
            printf("%s\n",msg);/*在屏幕上打印出来 */  
            strcpy(msg,"hi,I am server!");  
            send(cli_sockfd, msg, sizeof(msg),0); /*发送的数据*/  
            close(cli_sockfd);  
        }  
    }  
    close(ser_sockfd);  
    return 0;  

 }

客户端

#include <stdio.h>
#include <sys/types.h>  
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
 
 
#define MAX_MSG_SIZE 256
#define SERVER_PORT  9987
 
 
int GetServerAddr(char * addrname){  
    printf("please input server addr:");  
    scanf("%s",addrname);  
    return 1;  
}  
 
 
int main(){  
    int cli_sockfd;/*客户端SOCKET */  
    int addrlen;  
    char seraddr[14];  
    struct sockaddr_in ser_addr,/* 服务器的地址*/  
    cli_addr;/* 客户端的地址*/  
    char msg[MAX_MSG_SIZE];/* 缓冲区*/  
  
    GetServerAddr(seraddr);  
  
    cli_sockfd=socket(AF_INET,SOCK_STREAM,0);/*创建连接的SOCKET */  
  
    if(cli_sockfd<0){/*创建失败 */  
        fprintf(stderr,"socker Error:%s\n",strerror(errno));  
        exit(1);  
    }  
    /* 初始化客户端地址*/  
    addrlen=sizeof(struct sockaddr_in);  
    bzero(&ser_addr,addrlen);  
    cli_addr.sin_family=AF_INET;  
    cli_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
    cli_addr.sin_port=0;  
    if(bind(cli_sockfd,(struct sockaddr*)&cli_addr,addrlen)<0){  
        /*绑定失败 */  
        fprintf(stderr,"Bind Error:%s\n",strerror(errno));  
        exit(1);  
    }  
    /* 初始化服务器地址*/  
    addrlen=sizeof(struct sockaddr_in);  
    bzero(&ser_addr,addrlen);  
    ser_addr.sin_family=AF_INET;  
    ser_addr.sin_addr.s_addr=inet_addr(seraddr);  
    ser_addr.sin_port=htons(SERVER_PORT);  
    if(connect(cli_sockfd,(struct sockaddr*)&ser_addr, addrlen)!=0)/*请求连接*/  
    {  
        /*连接失败 */  
        fprintf(stderr,"Connect Error:%s\n",strerror(errno));  
        close(cli_sockfd);  
        exit(1);  
    }  
    strcpy(msg,"hi,I am client!");  
    send(cli_sockfd, msg, sizeof(msg),0);/*发送数据*/  
    recv(cli_sockfd, msg, MAX_MSG_SIZE,0); /* 接受数据*/  
    printf("%s\n",msg);/*在屏幕上打印出来 */  
    close(cli_sockfd);  
  
    return 0;  
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文艺小少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值