# socket 网络编程

# socket 介绍

Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。

服务器端先初始化 Socket,然后与端口绑定 (bind),对端口进行监听 (listen),调用 accept 阻塞,等待客户端连接。在这时如果有个客户端初始化一个 Socket,然后连接服务器 (connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

# 网络中进程之间通信

网络层重的 IP 地址可以作为唯一的标识,而传输层的协议 + 端口可以作为主机中唯一的应用程序,利用三元组(ip 地址,协议,端口)就可以标识网络的进程。

使用 TCP/IP 协议的应用程序通常采用应用编程接口:UNIX BSD 的套接字(socket)

# socket 基本操作

# socket 函数

int socket(int domain,int type,int protocol);

**socket ()** 用于创建一个 socket 描述符(socket descriptor),它唯一标识一个 socket。这个 socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

  • domain: 协议族,常用协议族 ** AF_INETAF_INET6AF_LOCAL ( AF_UNNIX )、 AF_ROUTE **
  • type: 指定 socket 类型。常用的 socket 类型有, SOCK_STREAM 、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET
  • protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC 等,它们分别对应 TCP 传输协议、UDP 传输协议、STCP 传输协议、TIPC 传输协议

协议族:

  • AF_INET:(TCP/IP-IPv4)
  • AF_INET6(TCP/IP – IPv6)
  • AF_UNIX(本机通信)

socket 类型:

  • SOCK_STREAM:流格式套接字也叫 “面向连接的套接字”。 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

  • SOCK_DGRAM: 数据报格式套接字(Datagram Sockets)也叫 “无连接的套接字 “。

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

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

    # binf () 函数

    bind () 函数把一个地址族中的特定地址赋给 socket。

    int bind (int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    
    • sockfd:即 socket 描述字,它是通过 socket () 函数创建了,唯一标识一个 socket。bind () 函数就是将给这个描述字绑定一个名字。

    • addr:一个 const struct sockaddr * 指针,指向要绑定给 sockfd 的协议地址。这个地址结构根据地址创建 socket 时的地址协议族的不同而不同,如 ipv4 对应的是:

    • struct sockaddr_in {
          sa_family_t    sin_family; 
          in_port_t      sin_port;   
          struct in_addr sin_addr;   
      };
      
      sockaddr_in seraddr;
      	seraddr.sin_addr.s_addr = INADDR_ANY;//监听任意地址
      	seraddr.sin_port = htons(80);//绑定端口
      	seraddr.sin_family = AF_INET;//绑定协议族
      

      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;  
      };
      
    • addrlen:对应的是地址的长度。

    # 网络字节序和主机字节序

    主机字节序

    大端和小端模式:不同的 CPU 有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的 Big-Endian 和 Little-Endian 的定义如下:

    1. Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
    2. Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

    网络字节序

    4 个字节的 32 bit 值以下面的次序传输:首先是 0~7bit,其次 8~15bit,然后 16~23bit,最后是 24~31bit。这种传输次序称作大端字节序。** 由于 TCP/IP 首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。** 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

    # listen ()、connect () 函数

    如果是服务器端,则调用 listen () 来监听这个 socket,如果是客户端这时调用的则为 connect () 来发起请求。

    int listen(int sockfd,int backlog);
    int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    

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

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

    # accept () 函数

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

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    

    accept 函数的第一个参数为服务器的 socket 描述字,第二个参数为指向 struct sockaddr * 的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果 accpet 成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接。

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

    # close 函数

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

    int close(int fd);
    

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

    # socket 中的 TCP 的三次握手建立和四次挥手

    三次握手

    1. 客户端向服务器发送一个 SYN J
    2. 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1
    3. 客户端再向服务器发一个确认 ACK K+1

当客户端调用 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 返回,至此三次握手完毕,连接建立。

四次挥手

  1. 假设 Client 端发起中断连接请求,也就是发送 FIN 报文。
  2. Server 端接到 FIN 报文后,意思是说 "我 Client 端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭 Socket,可以继续发送数据。所以你先发送 ACK,"告诉 Client 端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。
  3. 这个时候 Client 端就进入 FIN_WAIT 状态,继续等待 Server 端的 FIN 报文。当 Server 端确定数据已发送完成,则向 Client 端发送 FIN 报文,"告诉 Client 端,好了,我这边数据发完了,准备好关闭连接了"。
  4. Client 端收到 FIN 报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕 Server 端不知道要关闭,所以发送 ACK 后进入 TIME_WAIT 状态,如果 Server 端没有收到 ACK 则可以重传。“,Server 端收到 ACK 后," 就知道可以断开连接了 "。Client 端等待了 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,我 Client 端也可以关闭连接了。Ok,TCP 连接就这样关闭了!

# 完整代码

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/sendfile.h>
#include<fcntl.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
const int port = 8888;
int main(int argc,char *argv[])
{
    if(argc<0)
    {
        printf("need two canshu\n");
        return 1;
    }
    int sock;
    int connfd;
    struct sockaddr_in sever_address;
    bzero(&sever_address,sizeof(sever_address));
    sever_address.sin_family = PF_INET;
    sever_address.sin_addr.s_addr = htons(INADDR_ANY);
    sever_address.sin_port = htons(8888);
 
    sock = socket(AF_INET,SOCK_STREAM,0);
 
    assert(sock>=0);
 
    int ret = bind(sock, (struct sockaddr*)&sever_address,sizeof(sever_address));
    assert(ret != -1);
 
    ret = listen(sock,1);
    assert(ret != -1);
    while(1)
    {
        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof(client);
        connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
        if(connfd<0)
        {
            printf("errno\n");
        }
        else{
                char request[1024];
                recv(connfd,request,1024,0);
                request[strlen(request)+1]='\0';
                printf("%s\n",request);
                printf("successeful!\n");
                char buf[520]="HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";//HTTP响应
                int s = send(connfd,buf,strlen(buf),0);//发送响应
                //printf("send=%d\n",s);
                int fd = open("hello.html",O_RDONLY);//消息体
                sendfile(connfd,fd,NULL,2500);//零拷贝发送消息体
                close(fd);
                close(connfd);
        }
    }
    return 0;
}

哈哈哈哈哈

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Charmber 微信支付

微信支付

Charmber 支付宝

支付宝

Charmber 贝宝

贝宝