Linux 下的 Socket 编程详解 (一) 开始

简介

在 Linux 环境下,Socket 套接字是计算机操作系统中用来编写 TCP/IP 通信的接口。它是一种 facade 模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口谋面。在 TCP/IP 协议族里,Socket 的位置如下如所示:

OSI 模型和网际协议族中的映射

Socket 起源于 Unix,而 Unix 基本哲学就是“一切皆文件”,都可以用“open->write/read->close”模式来操作。Socket 就是该模式的一个实现,Socket 即是一种特殊的文件,一些 Socket 函数就是对其进行的操作(读/写IO、打、关闭)

  • 服务器端的套接字操作的流程为:socket -> bind -> listen -> accept -> write/read ->close

  • 客户端的套接字操作的流程为:socket -> connect -> write/read -> close

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

这些接口的实现都是内核来完成,我们的用户程序通过系统调用,使用内核接口,完成套接字的创建。

参考文档

一个最简单的例子

服务器端

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

#define PORT 12345


void Server(){

    int socket_fd, connect_fd;  // 套接字描述符
    struct sockaddr_in servaddr;
    char buff[4096];

    // 创建 socket
    if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
        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(PORT); // 设置端口

    // 将本地地址 bind 到 socket 上
    if(bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        std::cout << "绑定套接字失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    // 开始 listen 客户端连接
    if(listen(socket_fd, 10) == -1){
        std::cout << "开启监听失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    std::cout << "Waiting ......" << std::endl;

    // 阻塞直到有客户端连接, accept
    if((connect_fd = accept(socket_fd, (struct sockaddr*) nullptr, nullptr)) == -1){
        std::cout << "接受连接失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }


    // 接受客户端发送来的数据
    int n = recv(connect_fd, buff, sizeof(buff), 0);
    buff[n] = '\0';
    std::cout << buff << std::endl;

    // 向客户端发回数据
    char sendBack[] = "I Received";
    send(connect_fd, sendBack, strlen(sendBack), 0);

    close(connect_fd);
    close(socket_fd);
}

客户端

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

#define PORT 12345

void Client() {

    int socket_fd;
    char buff[] = "hello";
    struct sockaddr_in servaddr;


    if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        std::cout << "套接字创建失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 将点分十进制IP地址转换成整数

    // 创建连接
    if(connect(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
        std::cout << "连接创建失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }


    // 发送数据到服务器端
    if(send(socket_fd, buff, strlen(buff), 0) < 0){
        std::cout << "数据发送失败, 错误: " << strerror(errno) << errno << std::endl;
        exit(0);
    }

    // 接受服务器发来的数据
    char sendBack[2048];
    int n = recv(socket_fd, sendBack, sizeof(sendBack), 0);
    sendBack[n] = '\0';
    std::cout << sendBack << std::endl;

    close(socket_fd);

}

代码解释

创建套接字(socket)

首先,不管在客户端还是在服务器端,都需要调用 socket 函数,创建套接字。调用函数后,会返回一个整形的数字,这个数字被称为文件描述符(因为套接字也是一个特殊的文件)。在后面的套接字操作中,会使用到这个描述符。

对于文件描述符和文件指针,可以去看另外的文章详解

int socket(int protofamily, int type, int protocol) 函数接受三个参数。

  1. protofamily :协议族,常用的协议族有:AF_INET(IPv4)AF_INET6(IPv6)AF_LOCAL(Unix 域 Socket)。协议族决定了 套接字的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 + PORT 的组合
  2. type :指定套接字的类型
    • SOCK_STREAM
    • SOCK_DGRAM
    • SOCK_RAW
    • SOCK_PACKET
    • SOCK_SEQPACKET
  3. protocol :指定运输层的协议,包括IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC。分别对应 TCP,UDP,SCTP,TIPC

绑定 IP 地址(bind)

使用 bind 函数绑定 IP 地址,

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

  • sockfd :套接字描述符,通过 socket 创建,唯一标识一个 socket。bind 函数就是将给这个描述符绑定一个名字
  • addr :一个 const struct sockaddr* 指针,指向要绑定给 sockfd 的协议地址。其中传入这个参数的结构体如下:
// ipv4
struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

/* Internet address. */
struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

// ipv6
struct sockaddr_in6 { 
    sa_family_t     sin6_family;   /* AF_INET6 */ 
    in_port_t       sin6_port;     /* port number */ 
    uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
    struct in6_addr sin6_addr;     /* IPv6 address */ 
    uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};
  • addrlen :对应地址的长度

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

监听(listen)和连接(connect)

绑定 ip 地址后,服务器端就开启接听,等待客户端的连接。客户端创建套接字后,就通过指定 ip 地址连接服务器。

int listen(int sockfd, int backlog)

  • sockfd :套接字描述符
  • backlog :可以排队的最大连接个数

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

与服务器端的 bind 函数参数一样

应答(accept)

当有客户端 connect 到服务器,这时 accept 函数就会有响应,接受客户端的请求,这样连接就建立好了。之后就可以开始网络 I/O 操作,类似于不同的文件的读写 I/O。accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。

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

  • sockfd :套接字描述符
  • addr :这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
  • addrlen :上面 addr 结构的大小,同样也可置为 NULL

读(read)写(write)

建立连接后,就可以开始网络 I/O 的读写。网络 I/O 操作有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

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);

recv 函数从套接字描述符中读取内容,读取成功时,返回实际所读的字符数,如果小于 0 表示出错,send 函数同理。

flag :默认为 0。其他取值如下:

  • MSG_DONTROUTE 绕过路由表查找,send 独占取值
  • MSG_DONTWAIT 仅本操作非阻塞,send 和 recv 均可设置
  • MSG_OOB 发送或接收带外数据,send 和 recv 均可设置
  • MSG_PEEK 窥看外来消息,recv 独占取值
  • MSG_WAITALL 等待所有数据,recv 独占取值

关闭(close)

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

int close(int fd)

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