5月14日学习(socket)

什么是socket?

“通过socket编程,实现了网络终端接入模拟互联网和数据通信。”socket编程主要用于实现前端与互联网之间的数据传输,是实现通信的一种方式。

socket是一个接口,在用户进程与TCP/IP协议之间充当中间人,完成TCP/IP协议的书写,用户只需理解接口即可。
socket2

        原作者将socket通信类比为打电话这一生活场景。这里我把TCP服务器比作政府某一服务部门能,TCP客户端比作企业中某一部门电话,描述这一过程,恰好就像是socket通信,服务部门提供服务,企业部门申请服务。
        要实现通信,首先政府部门都必须申请一个电话(socket_fd),并向有关部门注册(我们的系统),提供地址(sockadrr)以及属于哪个部门的(port),录入系统后,就算是合约生效了(bind),于是乎,政府广而告之,这个服务热线就算开通了,在部门里面的人员所需要做的事情,就是等待企业家拨打热线(listen)。
        企业家拨打电话对地点和部门没有这么多的要求了,他并不需要绑定地址和部门,在任何一个可以拨打电话的地方(可能是同个部门,也可以同公司不同部门,甚至可能是竞争对手),他只需要拿起一个已经注册的电话(socket_fd),拨打电话(connect)政府部门接通电话(accept)后,桥梁就打通了(服务者client_fd、顾客server_fd),可以进行听说了(read write)。企业家咨询完成(close),政府到点下班关闭服务(close)
原文链接:https://blog.csdn.net/weixin_39258979/article/details/80835555 

socket简单算法

1.socket()创建socket描述符

int socket(int domain, int type, int protocol); 
//成功返回非负描述符,失败返回-1

 domain 网络层相关协议 ,type 信息传送方式 ,protocol 运输层相关协议,如果前两个都确定了,系统就可以自动选择协议了。

domain:即协议域,又称为协议族(family。
常用的地址族有:AF_INET          AF_INET6          AF_LOCAL(AF_UNIX,本地通信用)        AF_ROUTE
        协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:信息传送方式。

SOCK_STREAM
SOCK_DGRAM
SOCK_RAW
SOCK_PACKET
SOCK_SEQPACKET


protocol:对应协议。

IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP STCP传输协议
IPPROTO_TIPCTIPC传输协议
通常设置为0,让其自动匹配。

2.bind()绑定实际地址

int bind(int sockfd, const struct sockaddr *addr, socklen_t
addrlen); 
//返回值:成功则为0,失败为-1

sockfd 一般服务端才需要绑定,客户端由系统内核解决
addr 所有协议都有一个公共的结构叫做const struct sockaddr,不同协议对应不同的具体结构,结构如下:

struct sockaddr {  
     sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
   }; 

可以看出,我们端口和目标地址放在同一数组中,不太容易使用。对于不同的协议,我们会通过另一种特定的结构体的来完成初始化,再通过强制类型转换,来使用bind函数。

强制转换第二个地址参数为const sockaddr *

(const sockaddr *)sockaddr_in;
(const sockaddr *)sockaddr_in6;
(const sockaddr *)sockaddr_un;
  • ipv4结构体
    struct sockaddr_in {
        sa_family_t    sin_family; 
        in_port_t      sin_port;   //typedef	__uint16_t		in_port_t;
        struct in_addr sin_addr;   
    };
    
    struct in_addr {
        uint32_t       s_addr;     
    };
    
  • ipv6结构体
    struct sockaddr_in6 { 
        sa_family_t     sin6_family;    
        in_port_t       sin6_port;      
        uint32_t        sin6_flowinfo;  
        struct in6_addr sin6_addr;      
        uint32_t        sin6_scope_id;  
    };
    
    struct in6_addr { 
        unsigned char   s6_addr[16];    
    };
    
  • Unix域结构体
  • #define UNIX_PATH_MAX    108
    
    struct sockaddr_un { 
        sa_family_t sun_family;                
        char        sun_path[UNIX_PATH_MAX];   
    };
    
    addrlen:协议结构体大小
    sizeof(sockaddr_in);//IPv4 in:internet
    sizeof(sockaddr_in6);//IPv6
    sizeof(sockaddr_un);//本地 un:unix
    

     listen()、connect() 主机监听、从机链接

  • int listen(int sockfd, int backlog);
    //返回值:成功则为0,失败为-1 
    
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    //返回值:成功则为0,失败为-1 
    

    listen决定需要开启的部门热线,connect拨打电话。

    listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

    connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

  • accept 建立链接

  • int accept(int sockfd, struct sockaddr *addr, socklen_t  *addrlen);
    //返回值:成功则为0,失败为-1 
    

    第一个参数为服务器的socket描述字,
    第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址
    第三个参数为协议地址的长度。

    如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。后面的读写操作都是根据这个返回值来完成的

  • read和write函数,读写

    ssize_t read(int fd, void *buf, size_t count);
           ssize_t write(int fd, const void *buf, size_t count);
    
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);
    
    ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
    ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    

    read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

    write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是 全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。

  •  close关闭服务

    int close(int fd);
    //返回值:成功则为0,失败为-1 
    

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

  • .Socket中TCP的建立(三次握手(这一块非常抽象,具体我还没看懂)

  • TCP协议通过三个报文段完成连接的建立,这个过程称为三次握手(three-way handshake),过程如下图所示。

  • 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
    一个完整的三次握手也就是: 请求---应答---再次确认。

    对应的函数接口:


  •         

    从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    我们可以通过网络抓包的查看具体的流程:

    比如我们服务器开启9502的端口。使用tcpdump来抓包:

     tcpdump -iany tcp port 9502

    然后我们使用telnet 127.0.0.1 9502开连接.:

    telnet 127.0.0.1 9502

    14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
    14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0  (2)
    14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0  (3)


    14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
    14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
    14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
    14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

  • 114:12:45.104687 时间带有精确到微妙

  • localhost.39870 > localhost.9502 表示通信的流向,39870是客户端,9502是服务器端

  • [S] 表示这是一个SYN请求

  • [S.] 表示这是一个SYN+ACK确认包: 

  • [.] 表示这是一个ACT确认包, (client)SYN->(server)SYN->(client)ACT 就是3次握手过程

  • [P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推

  • [F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起

  • [R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接

  • win 4099 是指滑动窗口大小

  • length 18指数据包的大小

  • 我们看到 (1)(2)(3)三步是建立tcp:

    第一次握手:

    14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378

    客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(syn=j)到服务器》

    syn包(syn=j) : syn的seq= 2927179378  (j=2927179378)

    第二次握手:

    14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,

    收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包:
    此时服务器主机自己的SYN:seq:y= syn seq 1721825043。
    ACK为j+1 =(ack=j+1)=ack 2927179379 

    第三次握手:

    14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,

    客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)

    客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accepte,也进行3次握手完成。

    连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。

    紫色背景的部分:

    IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7

    客户端向服务器发送长度为7个字节的数据,

    IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0

    服务器向客户确认已经收到数据

     IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18

    然后服务器同时向客户端写入数据。

     IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

    客户端向服务器确认已经收到数据

    这个就是tcp可靠的连接,每次通信都需要对方来确认。(对,这个就是tcp的连接QAQ)
     

  • TCP连接的终止(四次握手释放)

  • 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:

    由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

    (1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。

    (2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。

    (3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。

    (4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

    对应函数接口如图:

  •  

    过程如下:

  • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

  • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

  • 接收到这个FIN的源发送端TCP对它进行确认。

    这样每个方向上都有一个FIN和ACK。

    1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

    这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

    2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

    这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
     

  • 编程实例

        服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。

/* File Name: server.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
{
    int    socket_fd, connect_fd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;
    //初始化Socket
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
    printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }
    //初始化
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT
 
    //将本地地址绑定到所创建的套接字上
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }
    //开始监听是否有客户端连接
    if( listen(socket_fd, 10) == -1){
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }
    printf("======waiting for client's request======\n");
    while(1){
//阻塞直到有客户端连接,不然多浪费CPU资源。
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
        continue;
    }
//接受客户端传过来的数据
    n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
    if(!fork()){ /*紫禁城*/
        if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)
        perror("send error");
        close(connect_fd);
        exit(0);
    }
    buff[n] = '\0';
    printf("recv msg from client: %s\n", buff);
    close(connect_fd);
    }
    close(socket_fd);
}

客户端:

/* File Name: client.c */
 
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
 
#define MAXLINE 4096
 
 
int main(int argc, char** argv)
{
    int    sockfd, n,rec_len;
    char    recvline[4096], sendline[4096];
    char    buf[MAXLINE];
    struct sockaddr_in    servaddr;
 
 
    if( argc != 2){
    printf("usage: ./client <ipaddress>\n");
    exit(0);
    }
 
 
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
    printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
    exit(0);
    }
 
 
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8000);
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
    printf("inet_pton error for %s\n",argv[1]);
    exit(0);
    }
 
 
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
    printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }
 
 
    printf("send msg to server: \n");
    fgets(sendline, 4096, stdin);
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
    exit(0);
    }
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
       perror("recv error");
       exit(1);
    }
    buf[rec_len]  = '\0';
    printf("Received : %s ",buf);
    close(sockfd);
    exit(0);
}

(运行结果我就不般了,如果真自己做了的话可以去原文章最后看看        socket)

总结

这里主要学习了一种数据传输的方式socket,主要用于互联网和硬件之间的通信。附了一部分基础代码。实例部分是一个客服端和服务器的搭建。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值