Linux学习--socket通信

发布时间 2023-12-14 17:03:36作者: (喜欢黑夜的孩子)

 

 学习心得

  

socket通信学习心得

 

  socket通信的作用是什么?
为了实现不同主机之间的网络通信,Linux引进了socket通信

 

  socket通信的过程是怎样的?从客户端和主机端两个方面阐述
  从主机端开始说,必须先利用socket()函数建立一个socket套接字,然后定义一个socketaddr_in 结构体来保存主机端的IP地址和端口地址,再利用bing()函数命令将端口和IP和socket返回的文件描述符绑定。绑定后调用listen()函数创建监听等待队列。
之后在while循环中调用accept函数接受客户端发送来的连接请求,accept会返回一个新的文件描述符(监听套接字)来用于通信,连接成功后就可以调用send和recv来进行网络IO通信了(操作监听套接字)。
通常只会有1个监听套接字和多个连接套接字
再看客户端,同样利用socket()函数返回一个文件描述符,但客户端并不需要去bing()绑定一个端口IP,而是在connect()执行的时候系统就会自动分配,connect会向服务器发送连接请求,当连接成功就可以通过recv/send进行通信了。{当然可以通过sendto()/recvfrom直接省略connect的过程}

 

 

 

   TCP/IP参考模型

关于TCP/IP协议

TCP/IP协议是一个复制的协议,是由一组专业化协议组成的。这些协议包括IPTCPUDPARPICMP以及其他的一些被称为子协议的协议。

 

各层次在模型中的作用

·  网络接口层

  网络接口层是TCP/IP协议软件的最底层,负责将二进制流转换为数据帧,并进行数据帧的发送和接收。数据帧是网络传输的基本单元

  网络层

 

   网络层负责在主机之间的通信中选择数据报的传输路径,即路由。当网络层接收到传输层的请求后,传输某个具有目的地址信息的分组。该层把分组封装在IP数据报中,填入数据报的首部,使用路由算法来确定是直接交付数据报,还是把它传递给路由器,然后把数据报交给适当的网络接口进行传输。

 

  网络层还要负责处理传入的数据报,检验其有效性,使用路由算法来决定应该对数据报进行本地处理还是应该转发。

 

  如果数据报的目的机处于本机所在的网络,该层软件就会除去数据报的首部,再选择适当的运输层协议来处理这个分组。最后,网络层还要根据需要发出和接收ICMPInternet控制报文协议)差错和控制报文。

 

  传输层

  传输层负责提供应用程序之间的通信服务。这种通信又称为端到端通信。传输层要系统地管理信息的流动,还要提供可靠的传输服务,以确保数据到达无差错、无乱序。为了达到这个目的,传输层协议软件要进行协商,让接收方回送确认信息及让发送方重发丢失的分组。传输层协议软件把要传输的数据流划分为分组,把每个分组连同目的地址交给网络层去发送。

  应用层

应用层是分层模型的最高层,在这个最高层中,用户调用应用程序通过TCP/IP互联网来访问可行的服务。与各个传输层协议交互的应用程序负责接收和发送数据。每个应用程序选择适当的传输服务类型,把数据按照传输层的格式要求封装好向下层传输。

  综上可知,TCP/IP分层模型每一层负责不同的通信功能,整体联动合作,就可以完成互联网的大部分传输要求。

 

 

  TCP/IP模型的地址边界

  TCP/IP分层模型中有两大边界特性:一个是地址边界特性,它将IP逻辑地址与底层网络的硬件地址分开;一个是操作系统边界特性,它将网络应用与协议软件分开,如图8.2所示。

  TCP/IP分层模型边界特性是指在模型中存在一个地址上的边界,它将底层网络的物理地址与网络层的IP地址分开。该边界出现在网络层与网络接口层之间。

  

  网络层和其上的各层均使用IP地址,网络接口层则使用物理地址,即底层网络设备的硬件地址TCP/IP提供在两种地址之间进行映射的功能。划分地址边界的目的是为了屏蔽底层物理网络的地址细节,以便使互联网软件地址上易于实现和理解。

  

  IP层特性

  IP向上层提供统一的IP报文,使得各种网络帧或报文格式的差异性对高层协议不复存在。IP层是TCP/IP实现异构网互联最关键的一层。

 

  TCP/IP的重要思想之一就是通过IP将各种底层网络技术统一起来,达到屏蔽底层细节,提供统一虚拟网的目的

 

 

  TCP/IP的可靠性特性

  TCP/IP的可靠性体现在传输层协议之一的TCP协议。TCP协议提供面向连接的服务,因为传输层是端到端的,所以TCP/IP的可靠性被称为端到端可靠性

  综上可知,TCP/IP的特点就是将不同的底层物理网络、拓扑结构隐藏起来,向用户和应用程序提供通用、统一的网络服务。这样,从用户的角度看,整个TCP/IP互联网就是一个统一的整体,它独立于具体的各种物理网络技术,能够向用户提供一个通用的网络服务。

  TCP/IP网络完全撇开了底层物理网络的特性,是一个高度抽象的概念,正是由于这个原因,其为TCP/IP网络赋予了巨大的灵活性和通用性。

  

  TCP/IP协议族  

  

 

  TCP协议

  TCP向应用层提供可靠的面向对象的数据流传输服务TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递

  应用程序通过向TCP层提交数据接发送/收端的地址端口号而实现应用层的数据通

  

  通过IP的源/目的可以惟一地区分网络中两个设备的连接;

  通过socket的源/目的可以惟一地区分网络中两个应用程序的连接;

 

  TCP三次握手四次释放

  

  第一步(A->B):主机A向主机B发送一个包含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号;

 

  第二步(B->A):主机B在收到客户端的SYN报文后,将返回一个SYN+ACK的报文,表示主机B的请求被接受,同时TCP序号被加一,ACK即确认

  第三步(A->B):主机A也返回一个确认报文ACK给服务器端,同样TCP序列号被加一,到此一个TCP连接完成

 

  超时重传机制

  TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的TCP实体向回发送一个数据报,其中包含有一个确认序号,它意思是希望收到的下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据报。

  TCP报头格式

  

   

 

  UDP

  UDP即用户数据报协议,是一种面向无连接的不可靠传输协议,不需要通过3次握手来建立一个连接,同时,一个UDP应用可同时作为应用的客户或服务器方。

由于UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。UDPTCP协议更为高效,也能更好地解决实时性的问题,如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP协议。    

          

 

  套接字定义

  

  在Linux中的网络编程是通过socket接口来进行的。套接字(socket)是一种特殊的I/O接口,它也是一种文件描述符。socket是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信,而且通过网络能够在不同机器上的进程之间进行通信

  每一个socket都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接建立、数据传输等操作都是通过socket来实现的。

 

  常见的socket3种类型如下。

1流式套接字SOCK_STREAM

流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的可靠性和顺序性。

2数据报套接字SOCK_DGRAM

数据报套接字定义了一种无可靠、面向无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。它使用数据报协议UDP

3原始套接字SOCK_RAW

原始套接字允许对底层协议如IPICMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发

  

 

  地址和顺序处理

  下面首先介绍两个重要的数据类型:sockaddrsockaddr_in,这两个结构类型都是用来保存socket信息的,如下所示:

  

  两个结构体的效果是等效的,通常sockaddr_in更好用

struct sockaddr 
{
        unsigned short sa_family; /*地址族*/
        char sa_data[14]; /*14字节的协议地址,包含该socket的IP地址和端口号。*/
};
struct sockaddr_in 
{
        short int sa_family; /*地址族*/
        unsigned short int sin_port; /*端口号*/
        struct in_addr sin_addr; /*IP地址*/
        unsigned char sin_zero[8]; /*填充0 以保持与struct sockaddr同样大小*/
};

 

  sa_family可用参数

 

  什么是网络字节序 

  网络字节序(Network Byte Order)是一种规定好的字节序,用于在不同计算机之间传输数据时保证数据的正确性。在网络字节序中,数据的字节序是固定的,即采用大端字节序(Big Endian)。

  在网络通信中,为了保证数据的正确性,需要将数据转换为网络字节序后再传输

 

  数据存储优先顺序

  计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式,PC机通常采用小端模式)。

  Internet上数据以高位字节优先顺序在网络上传输

  因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了四个函数:htons()ntohs()htonl()ntohl()。这四个地址分别实现网络字节序和主机字节序的转化,这里的h代表hostn代表networks代表shortl代表long

 

头文件包含
#include <netinet/in.h>

uint16_t htons(unit16_t host16bit)
uint32_t htonl(unit32_t host32bit)
uint16_t ntohs(unit16_t net16bit)
uint32_t ntohs(unit32_t net32bit)

 

  IP地址转换

  用户在表达地址时通常采用点分十进制表示的数值字符串(或者是以冒号分开的十进制IPv6地址),而在通常使用的socket编程中所使用的则是二进制值(例如,用in_addr结构和in6_addr结构分别表示IPv4IPv6中的网络地址),这就需要将这两个数值进行转换

  这里在IPv4中用到的函数有inet_aton()inet_addr()inet_ntoa()

  

  inet_pton()函数是将点分十进制地址字符串转换为二进制地址(例如:将IPv4的地址字符串“192.168.1.123” 转换为4个字节的数据(从低字节起依次为1921681123))

 

int inet_pton(int family, const char *strptr, void *addrptr)
int inet_ntop(int family, void *addrptr, char *strptr, size_t len)

 

 

 

  

  inet_ntop()inet_pton()的反操向作,将二进制地址转换为点分十进制地址字符串

  

  

 

  名字/地址的转化

  在Linux中有一些函数可以实现主机名和地址的转化,如gethostbyname()gethostbyaddr()getaddrinfo()等,它们都可以实现IPv4IPv6的地址和主机名之间的转化

   其中

   gethostbyname()是将主机名转化为IP地址;

      gethostbyaddr()则是逆操作,是将IP地址转化为主机名;

   另外getaddrinfo()还能实现自动识别IPv4地址和IPv6地址。

 

struct hostent *gethostbyname(const char *hostname)

  涉及到的地址结构体

struct hostent
{
        char *h_name;            /*正式主机名*/
        char **h_aliases;        /*主机别名*/
        int h_addrtype;        /*地址类型*/
        int h_length;            /*地址字节长度*/
        char **h_addr_list;    /*指向IPv4或IPv6的地址指针数组*/
}

  调用该函数时可以首先对hostent结构体中的h_addrtypeh_length进行设置,若为IPv4可设置为AF_INET4;若为IPv6可设置为AF_INET6为6;若不设置则默认为IPv4地址类型

 

 

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **result)

在调用之前,首先要对hints服务线索进行设置。

 

struct addrinfo
{
        int ai_flags;                /*AI_PASSIVE, AI_CANONNAME;*/
        int ai_family;            /*地址族*/
        int ai_socktype;            /*socket类型*/
         int ai_protocol;            /*协议类型*/
        size_t ai_addrlen;        /*地址字节长度*/
        char *ai_canonname;        /*主机名*/
        struct sockaddr *ai_addr;    /*socket结构体*/
        struct addrinfo *ai_next;    /*下一个指针链表*/
}    

  addrinfo常见选项

#include <netdb.h>

 

 

1.1.1 套接字编程

  socket编程的基本函数有socket()bind()listen()accept()send()sendto()recv()以及recvfrom()等,其中根据客户端还是服务端,或者根据使用TCP协议还是UDP协议,这些函数的调用流程都有所区别

  

  各个函数的作用以及工作流程

  

 

 

 

              socket()函数语法要点

#include <sys/socket.h>
int socket(int family, int type, int protocol)

 

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

 

 

 

#include <sys/socket.h>
int listen(int sockfd, int backlog)

 

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

 accept函数是一个会阻塞的函数,在没有连接请求到来时会一直阻塞在此处,当有连接请求时阻塞解除并返回一个文件描述符,通过该文件描述符就可以和其它主机通信了

 

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)

 

 

#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags)

 

 

#include <sys/socket.h>
int recv(int sockfd, void *buf,int len, unsigned int flags)

 

该函数也是一个会阻塞的函数

连接建立没有断开时,接收端没有收到数据时recv会阻塞直到有数据接入 。

连接断开了以后,没有数据时recv则不会阻塞而是直接返回0

 

#include <sys/socket.h>
int sendto(int sockfd, const void *msg,int len, unsigned int flags, const struct sockaddr *to, int tolen)

 

#include <sys/socket.h>
int recvfrom(int sockfd,void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen)

 

  异步IO配合网络通信编程

  内核通过使用异步I/O,在某一个进程需要处理的事件发生(例如,接收到新的连接请求)时,向该进程发送一个SIGIO信号。这样,应用程序不需要不停地等待着某些事件的发生,而可以往下运行,以完成其它的工作。只有收到从内核发来的SIGIO信号时,去处理它(例如,读取数据)就可以。

 

 

 

  使用例

服务器端

#include <string.h>
#include <arpa / inet.h>
#include <netinet / in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys / types.h>
#include <sys / socket.h>
#include <errno.h>

#define N 64

int main(int argc, char * argv[])//./server ip port
{
    //定义监听和连接文件描述符
    int listenfd, connfd;
    struct sockaddr_in myaddr, peeraddr;
    socklen_t len;
    char buf[N] = { 0 };
    ssize_t n;

    //如果输入参数数量少于3,报错返回
    if (argc  < 3)
    {
        printf("usage:%s ip port\n", argv[0]);
        return 0;
    }
    //建立一条类型为Ipv4的TCP连接套接字端口,-1表示出错
    if ((listenfd  = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(-1);
    }

    memset(&myaddr, 0, sizeof(myaddr));
    //Ipv4类型连接
    myaddr.sin_family  = AF_INET;
    //atoi:C库中的函数,用于将字符串数字转化为整数
    //myaddr.sin_port是端口号,必须存储网络号(网络字节序),htons实现将主机字节序转化为网络字节序
    //这句的意思是将argv[2]传入的IP地址转化为整数后再从主机号转化为网络号(端口号)
    myaddr.sin_port  = htons(atoi(argv[2]));//"6000"--6000 htons(6000);
    //inet_addr 是一个函数,用于将一个字符串表示的IPv4地址转换为一个32位的网络字节序整数
    //保存一个ipv4的地址
    myaddr.sin_addr.s_addr  = inet_addr(argv[1]);

    //将myaddr结构体的内容与listenfd套接字进行连接
    if (bind(listenfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1)
    {
        perror("bind");
        exit(-1);
    }
    //调用listen会创建一条等待序列存放等待连接的客户端请求,最大容纳5个等待请求
    if (-1 == listen(listenfd, 5))
    {
        perror("listen");
        exit(-1);
    }

    memset(&peeraddr, 0, sizeof(peeraddr));
    len  = sizeof(peeraddr);

    while (1)
    {
        //从监听的等待队列中取出一个请求进行处理
        if ((connfd  = accept(listenfd, (struct sockaddr *)&peeraddr, &len)) == -1)
        {
            perror("accept");
            exit(-1);
        }
        //二进制转化为10进制,主机字节序转化为网络字节序
        printf("from %s:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        while (1)
        {
            memset(buf, 0, sizeof(buf));
            //接收客户端发来的消息到buf中,返回接收到的个数
            n  = recv(connfd, buf, N, 0);//a b c \n  n=4
            if (n  == 0)
                break;
            buf[n] = '\0';
            printf("n=%d %s", n, buf);
            //从新将缓冲区的内容发送回去
            send(connfd, buf, n, 0);
        }

        close(connfd);
    }

    return 0;
}

 

客户端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys / types.h>
#include <sys / socket.h>
#include <errno.h>
#include <string.h>
#include <arpa / inet.h>
#include <netinet / in.h>

#define N 64

int main(int argc, char * argv[])//./server ip port
{
    int sockfd;
    struct sockaddr_in servaddr;
    char buf[N] = { 0 };

    if (argc  < 3)
    {
        printf("usage:%s ip port\n", argv[0]);
        return 0;
    }

    //建立TCP连接的IPV4连接的套接字端口
    if ((sockfd  = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        exit(-1);
    }


    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family  = AF_INET;    //IPV4地址
    servaddr.sin_port  = htons(atoi(argv[2]));//"6000"--6000 htons(6000);    //存放命令的第二个参数-端口8888
    servaddr.sin_addr.s_addr  = inet_addr(argv[1]);    //inet_addr()用于将点分10进制的ip转化为网络字节序,方便后续传输


    while (1)
    {
        printf(">");
        //从标准输入中采集N个数据
        fgets(buf, N, stdin);
        //sockfd :套接字 , buf :要发送的数据缓冲区 ,缓冲区大小,flag(默认0),要发送的地址,地址大小
        //sendto:该函数会自动寻找地址发送连接请求
        sendto(sockfd, buf, strlen(buf) + 1, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

        memset(buf, 0, sizeof(buf));
        //N:接收数据个数 0:flag  NULL,NULL :等同于recv(不选择特定连接对象)
        recvfrom(sockfd, buf, N, 0, NULL, NULL);
        printf("%s\n", buf);
    }
    //关闭套接字
    close(sockfd);

    return 0;
}

 

利用select实现多路IO复用

客户端

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys / types.h>
#include <sys / socket.h>
#include <errno.h>
#include <strings.h>
#include <netinet / in.h>
#include <arpa / inet.h>

typedef struct sockaddr SA;
#define N 64

int main(int argc, char * argv[])
{
int sockfd;
ssize_t n;
struct sockaddr_in servaddr;
char buf[N] = { 0 };

if (argc  < 3)
{
fprintf(stdout, "usage:%s ip port\n", argv[0]);
exit(0);
}

if ((sockfd  = socket(PF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(-1);
}

//bzero()函数将某段内存区清0,等同于memset
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family  = PF_INET; //PF_INET:套接符族,包括IPV4
servaddr.sin_port  = htons(atoi(argv[2])); // "9000"---9000
servaddr.sin_addr.s_addr  = inet_addr(argv[1]); //要绑定的IP地址

//绑定套接字和IP,端口
if (connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) == -1)
{
perror("connect");
exit(-1);
}

printf(">");
while (fgets(buf, N, stdin) != NULL)//abc\n
{
buf[strlen(buf) - 1] = 0; //abc\0
send(sockfd, buf, N, 0);

bzero(buf, sizeof(buf));
n  = recv(sockfd, buf, N, 0);
printf("n=%d buf=%s\n", n, buf);
printf(">");
}

close(sockfd);

exit(0);
}

服务器端

#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>

#define N 64

int main(int argc, char *argv[])//./server ip port
{

    int listenfd, connfd;
    struct sockaddr_in myaddr, peeraddr;
    socklen_t len;
    char buf[N] = {0};
    ssize_t n;

    if (argc < 3)
    {
        printf("usage:%s ip port\n", argv[0]);
        return 0;
    }

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket");
        exit(-1);
    }

    memset(&myaddr, 0sizeof(myaddr));
    myaddr.sin_family = AF_INET;
    myaddr.sin_port = htons(atoi(argv[2]));//"6000"--6000 htons(6000);
    myaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if (bind(listenfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1)
    {
        perror("bind");
        exit(-1);
    }

    if (-1 == listen(listenfd, 5))
    {
        perror("listen");
        exit(-1);
    }

    memset(&peeraddr, 0sizeof(peeraddr));
    len sizeof(peeraddr);

    while (1)
    {
        if ((connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &len)) == -1)
        {
            perror("accept");
            exit(-1);
        }
        printf("from %s:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        while (1)
        {
            memset(buf, 0sizeof(buf));
            n = recv(connfd, buf, N, 0);//a b c \n  n=4
            if (n == 0)
                break;
            buf[n] '\0';
            printf("n=%d %s", n, buf);

            send(connfd, buf, n, 0);
        }

        close(connfd);
    }

    return 0;
}