Socket编程

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. 完善回射客户/服务器程序

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 1 命运对人最严厉的惩罚,就是让他得到一切,最终却一无所有。 命运对我们最常做的事,也是一次次给我们慷慨的馈赠,再...
    人神共奋阅读 2,104评论 2 12
  • 若同时追两只兔子,一只也抓不到。 这就是本书的风格,浅显易懂却大都忽视朴素道理展开。 我在双十一前后买的,预定一个...
    熊芳菲阅读 206评论 0 1
  • 犹豫半天的她,终于开口说话。 “那阵子,我和男朋友正考虑结婚时,爷爷去世了,我们一起回了老家。我发现,我不爱他了。...
    我的心灵大白阅读 2,783评论 6 5
  • 此时此刻,待在一个书吧的一个角落,耳边萦绕着当前流行的乐曲,我蜷缩着,仿佛世界与我无关,但心不许~ 刚出社会的时...
    我们都会好阅读 157评论 0 0
  • 在看到别人打针着,不知道你有没有一种感觉,虽然针不扎在你身上,但看着那针扎入别人皮肤的那一刻,你看着都感觉疼。 镜...
    JOY121阅读 433评论 3 5