Socket编程

作者: 编程半岛 | 来源:发表于2018-08-05 20:28 被阅读8次

1. 什么是socket?

  • socket可以看成是用户进程内核网络协议栈的编程接口;
  • socket不仅可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信
  • 每一个套接口有一个地址属性

2. IPv4套接口

  • IPv4套接口地址结构通常也称为网际套接字地址结构,它以sockaddr_in命名,定义在头文件<netinet/in.h>中:
struct sockaddr_in {
    uint8_t sin_len;
  sa_family_t sin_family;
  in_port_t sin_port; /* 端口*/
  struct in_addr sin_addr; /* Internet 地址 */
  char sin_zero[8]; 
};
  • sin_len:整个sockaddr_in结构体的长度,有些没有这个;
  • sin_family:指定该地址家族,在这里必须设为AF_INETAF_INET6表示IPv6地址族)
  • sin_port端口,无符号16位整数,最大值为65535
  • sin_addrIPv4的地址,无符号32位整数
  • sin_zero:暂不使用,一般将其设置为0
    通过man帮助手册参看地址结构:man 7 ip

3. 通用的地址结构

  • 通用地址结构用来指定与套接字关联的地址:
struct sockaddr {
    uint8_t sin_len;
  sa_family_t  sa_family; /* 地址家族, AF_xxx */
  char sa_data[14]; /*14字节协议地址*/
};
  • sin_len:整个sockaddr_in结构体的长度,有些没有这个;
  • sin_family:指定该地址家族
  • sa_data:由sin_family决定它的形式

4. 网络字节序

  • 大端字节序(Big Endian):最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址处;
  • 小端字节序(Little Endian):最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址处;
  • 主机字节序:不同的主机有不同的字节序;
  • 网络字节序:网络字节序规定为大端字节序;
    socket编程支持异构系统,不同的操作系统的字节序可能不相同,因此传输过程中统一成网络字节序

测试本机操作系统是大端还是小端:

#include <stdio.h>

using namespace std;

int main()
{
    unsigned int a = 0x12345678;

    unsigned char* p = (unsigned char*)&a;

    printf("%0x, %0x, %0x, %0x\n", p[0], p[1], p[2], p[3]);

    return 0;
}

5. 字节序转换函数

uint16_t htons(uint16_t hostshort)--"Host to Network Short"
uint32_t htonl(uint32_t hostlong)--"Host to Network Long"
uint16_t ntohs(uint16_t netshort)--"Network to Host Short"
uint32_t ntohl(uint32_t netlong)--"Network to Host Long"

6. 地址转换函数

点分十进制地址与32为整数地址转换
头文件:
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char* cp, struct in_add* inp);
in_addr_t inet_addr(const char* cp); // 将点分十进制地址转换为32位整数
char* inet_ntoa(struct in_addr in); // 将地址结构(32位整数)转换为点分十进制

#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    unsigned long addr = inet_addr("192.168.0.100");    // 将点分十进制地址转换为32位整数 
    printf("addr = %u\n", ntohl(addr));                 // 将32位整数转化为网络字节序

    
    struct in_addr ipaddr;
    ipaddr.s_addr = addr;
    printf("%s\n", inet_ntoa(ipaddr));                  // 将地址结构(32位整数)转换为点分十进制
    
    return 0;
}

7. 套接字类型

  • 流式套接字SOCK_STREAM(TCP协议):提供面向连接的,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收
  • 数据报式套接字SOCK_DGRAM(UDP协议):提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱
  • 原始套接字SOCK_RAW:将应用层的数据跨越传输层,直接封装成网路层的数据

8. TCP客户/服务器模型(C/S模型)

C/S模型

9. 回射客户/服务器模型

回射客户/服务器模型

10. socket函数

  • 头文件:<sys/socket.h>
  • 功能:创建一个套接字用于通信
  • 原型:int socket(int domain, int type, int protocol);
    domain:指定通信协议族(protocol family)
    type:指定socket类型,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW
    protocol:协议类型
  • 返回值:成功返回非负整数,它与文件描述符类似,我们将它称为套接字描述字,简称套接字。失败返回-1

man帮助手册输入man socket即可获得帮助文档

    // 创建套接字
    int listenfd;
    if ( (listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))  < 0 )  // PF_INET(AF_INET):IPv4 Internet protocols, SOCK_STREAM:流式套接字
    {
        ERR_EXIT("socket");
    }

11. bind函数

  • 头文件:<sys/socket.h>
  • 功能:绑定一个本地地址到套接字
  • 原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数:
    sockfd:socket函数返回的套接字
    addr:要绑定的地址
    addrlen:地址长度
  • 返回值:成功返回0, 失败返回-1

man帮助手册输入man bind即可获得帮助文档

    // 地址的初始化
    struct sockaddr_in servaddr;    // IPv4的地址结构
    memset(&servaddr, 0, sizeof(servaddr)); // 初始化地址
    servaddr.sin_family = AF_INET;      // 地址族
    servaddr.sin_port = htons(5188);    // 指定端口号,并将端口号转化为2个字节的网络字节序
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // 绑定主机任意地址
    //servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");        // 指定地址  
    //inet_aton("127.0.0.1", &servaddr.sin_addr);       // 指定地址

    // 将套接字与地址绑定
    if( bind(listenfd, (struct sockaddr*)(&servaddr), sizeof(servaddr)) < 0 )
    {
        ERR_EXIT("bind");
    }

12. listen函数

  • 头文件:<sys/socket.h>
  • 功能:将套接字用于监听进入的连接
  • 原型:int listen(int sockfd, int backlog);
  • 参数:
    sockfd:socket函数返回的套接字
  • backlog:规定内核为此套接字排队的最大连接个数

调用listen函数后套接字变为被动套接字

  • 被动套接字:接收连接(调用accept函数接收连接)
  • 主动套接字(默认):用来发起连接(调用connect3函数发起连接)
    // 监听
    if( listen(listenfd, SOMAXCONN) < 0 )
    {
        ERR_EXIT("listen");
    }
  • 一般来说,listen函数应该在调用socket函数和bind函数之后,调用accept函数之前调用
  • 对于给定的监听套接口,内核需要维护两个队列:1.已由客户发出并到达服务器,服务器正在等待完成相应的TCP三次握手过程 2. 已完成连接的队列

13. accept函数

  • 头文件:<sys/socket.h>
  • 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
  • 原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数:
    sockfd:服务器套接字
    addr:将返回对等方的套接字地址
    addrlen:返回对等方的套接字地址长度
  • 返回值:成功返回非负整数,失败返回-1
    // 接收
    struct sockaddr_in peeraddr;    // 对方的地址
    socklen_t peerlen = sizeof(peeraddr);
    int conn;       // 定义已连接套接字
    if( accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen) < 0 )
    {
        ERR_EXIT("accept");
    }

14. connect函数

  • 头文件:<sys/socket.h>
  • 功能: 建立一个连接至addr所指定的套接字
  • 原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd:未连接的套接字
    addr:要连接的套接字地址
    addrlen:地址长度
  • 返回值:成功返回0, 失败返回-1
    // 连接 connect
    if( connect(sock, (struct sockaddr*)(&cliaddr), sizeof(cliaddr))  < 0 )
    {   
        ERR_EXIT("connect");
    }

15. REUSEADDR

  • 服务器端尽可能使用REUSEADDR
  • 在绑定之前尽可能调用setsockopt来设置REUSEADDR套接字选项
  • 使用REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器
    // 设置地址重复利用 REUSEADDR
    int on = 1;
    if( (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))  < 0) )
    {
        ERR_EXIT("setsockopt");
    } 

16. process-per-connection

  • 一个连接一个进程来处理并发
  • 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果。父进程用来监听,子进程用来处理通信
    // 通过多进程来使得服务器可以接收多个客户端的消息,从而达到并发效果
    // 父进程用来监听,子进程用来处理通信
    pid_t pid;
    while(true)
    {
        if( (conn = accept(listenfd, (struct sockaddr*)(&peeraddr), &peerlen)) < 0 )
        {
            ERR_EXIT("accept");
        }

        printf("ip=%s, port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); // 打印客服端发送过来的ip和port

        pid = fork();
        if( pid == -1 )
        {
            ERR_EXIT("fork");
        }
        else if( pid == 0 )     // 子进程来处理通信
        {
            close(listenfd);    // 子进程不需要监听,将监听套接口关闭
            do_servece(conn);   // 通信处理函数
            exit(EXIT_SUCCESS); // 当客户端关闭后,子进程销毁
        }
        else    // 父进程
        {
            close(conn);    // 父进程不需要通信,将通信套接口关闭
        }
    }

// 通信处理函数
void do_servece(int conn)
{
    char recvbuf[1024];
    while(true)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        if( ret == 0 )          // 返回值为0,表示客户端关闭
        {
            printf("Client close");
            break;              // 当客户端关闭,退出循环
        }
        else if( ret == -1)
        {
            ERR_EXIT("read");
        }
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
}

17. 点对点的聊天程序实现

  • 即两端能够聊天通信
  • 实现原理:在服务器和客户端都创建两个进程,一个用来接收数据,一个用来发送数据
    源码

18. 流协议与粘包问题

  • TCP为字节流传输,无边界

19. 粘包产生的原因

20. 粘包处理方案

21. readn 和 writen

22. 完善回射客户/服务器程序

相关文章

网友评论

    本文标题:Socket编程

    本文链接:https://www.haomeiwen.com/subject/reybvftx.html