TCP 套接字编程

基本TCP客户服务器模型

基本TCP客户服务器模型.jpg

IPV4套接字结构

  • struct sockaddr

    /* 通用套接字地址 */
    #include <sys/socket.h>
    
    struct sockaddr 
    {
        uint8_t       sa_len;          /* 1字节 */
        sa_family_t   sa_family;       /* 1字节 */
        char          s_data[14];      /* 14字节 包含目标地址和端口信息 */
    };
    
  • struct sockaddr_in

    /* IPV4地址,网络字节序 */
    typedef uint32_t in_addr_t;
    struct in_addr 
    {
        in_addr_t s_addr;    /* address in network byte order */
    };
    
    /* IPV4套接字地址 */
    #include <netinet/in.h>
    
    struct sockaddr_in 
    {
        sa_family_t    sin_family;         /* address family: AF_INET   */
        uint16_t       sin_port;           /* port in network byte order  2字节 */
        struct         in_addr sin_addr;   /* internet address  4 字节 */
        char           sin_zero[8];
    };
    
struct sockaddr 和 struct sockaddr_in 区别和联系
  • 2者都是16字节长度
  • connect 等后面系统调用使用的地址是 sockaddr

socket 函数

#include <sys/socket.h>

int socket(int family, int type, int protocol)  成功返回非负描述符,若出错返回-1

family指明协议族:PF_INETPF_INET6PF_LOCAL....
type指明套接字类型:SOCK_STREAM(TCP),SOCK_DGRAM(UDP),SOCK_RAW(原始套接字)
protocol指明某个协议类型的常值,现在基本废弃,一般设为0。

socket成功返回套接字描述符。

  • 示例:socket函数直接创建一个非阻塞的ipv4套接字
    int createSocket()
    {
        int ret = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
        if (ret == -1) {
            SYSFATAL("socket() Error");
        }
        return ret;
    }
    

bind 函数

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *myaddr, socklen_ addrlen);    
若成功则为0,若出错则为-1

bind函数把一个本地协议地址赋予一个套接字。

struct sockaddr_in addr_;
addr_.sin_family = AF_INET;
in_addr_t ip = INADDR_ANY // INADDR_LOOPBACK 
addr_.sin_port = htons(port);
addr_.sin_addr.s_addr = htonl(ip);
如何选择绑定地址
  • IP地址选择
    INADDR_ANY 表示地址是 0.0.0.0 (主机序的数值表达形式)
    INADDR_LOOPBACK 表示 127.0.0.1 (主机序的数值表达形式)

    假设一台机器对外访问的ip地址是120.55.94.78,这台机器在当前局域网的地址是192.168.1.104;同时这台机器有本地回环地址127.0.0.1。

    如果你指向本机上可以访问,那么你 bind 函数中的地址就可以使用127.0.0.1 或 INADDR_LOOPBACK
    如果你的服务只想被局域网内部机器访问,bind 函数的地址可以使用192.168.1.104。
    如果 希望这个服务可以被公网访问,你就可以使用地址 0.0.0.0 或 INADDR_ANY

  • 端口号选择
    如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口,bind 0 号端口也是一样的选择一个临时端口。

connect 函数

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
成功则为0, 若出错则为-1

connect用来与远程服务器建立连接。若是TCP套接字,调用connect将激发3路握手的过程,仅在连接建立成功或者出错时才返回

  • 若TCP客户没有收到SYN分节的响应,则返回 ETIMEDOUT 错误。
  • 若对客户的SYN的响应式RST,则表明服务器主机在我们指定的端口上没有进程在等待与之连接, 这是一种"硬错误",会立即返回 ECONNREFUSED 错误。

产生RST的3个条件:

  • 目的地为某端口SYN到达,而该端口没有上没有监听的服务、
  • TCP想取消一个已有连接
  • TCP接受到一个根本不存在的连接
Paste_Image.png

connect函数会导致当前套接字状态从 CLOSED 状态(该套接字自从由socket函数创建以来一直所处的状态)
转移到SYN_SENT状态,若成功再转移到 ESTABLISHED 状态。每次connect失败后,都必须close当前的套接字描述符并重新调用 socket。

关于阻塞和非阻塞的connect

在 socket 是阻塞模式下 connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),
在实际项目中,我们一般倾向使用所谓的异步的 connect 技术,或者叫非阻塞的 connect。此时调用 connect 函数,此时无论是否连接成功都会立即返回;如果返回 -1并不表示连接出错,此时的 errno 会被设置成EINPROGRESS

listen函数

#include <sys/socket.h>

int listen(int sockfd, int backlog);  若成功则为0,若出错则为-1
  • 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说将调用connect发起连接的客户套接字。而listen函数把一个未连接的套接字转换为一个被动套接字。
  • 理解backlog参数
TCP为监听套接字维护2个队列.jpg

内核为任何一个给定的监听套接字维护2个队列。

  • 未完成的连接队列,每一个SYN分节对应其中一项
  • 已完成的连接队列,每个已完成TCP三路握手过程的客户。

当来自客户的SYN到达时,TCP在 未完成连接队列 中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,捎带对客户SYN的ACK。
这一项一直保留在未完成连接队列中,直到3路握手的第三个分节(客户对服务器的SYN的ACK)到达或者该项超时。如果三路握手正常完成,该项就从未完成连接队列移到已完成的连接队列队尾。这里的backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小。

accept 函数

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
成功返回非负描述符, 若出错返回-1

accept函数由TCP服务器调用,用于从已完成连接队列头部返回一个已经完成连接的客户,如果已完成连接队列为空,则该进程会被投入睡眠。成功,返回值是一个由内核产生的全新描述符,代表与客户的连接,称之为已连接套接字。

close函数

#include <unistd.h>

int close(int sockfd);      返回:若成功则为0,如出错则为-1

close这个函数会对套接字引用计数减1,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭 TCP 两个方向的数据流。

在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。

shutdown函数

#include <sys/socket.h>

int shutdown(int sockfd, int howto);   返回值:成功为0,出错则为-1

函数行为依赖于howto参数的值。

  • SHUT_RD:关闭连接的读的一半
    套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。
  • SHUT_WR:关闭连接的写这一半--这称为半关闭
    当前留在套接字发送缓冲区的数据将被发送掉,然后TCP正常连接终止序列。
  • SHUT_RDWR:连接的读半部和写半部都关闭。

shutdown函数和close函数相比,close有2个限制:

  • close函数把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。shutdown函数不管引用计数都会激发TCP的正常连接终止序列。
  • close终止读和写2个方向的数据传送。

inet_pton和inet_ntop函数

#include <arpa/inet.h> 

int inet_pton(int family, const char* strptr, void *addrptr); 
    返回值:若成功则为1,若输入不是有效的表达格式则为0,若出错则为1,且errno置为EAFNOSUPPORT
const char* inet_ntop(int family, const void* addrptr, char *strptr, size_t len);
    返回值:若成功则为指向结果的指针,若出错则为NULL,且errno置为EAFNOSUPPORT。

eg:  inet_pton(AF_INET, ip_addr, &(cliname.sin_addr));
eg:  inet_ntop(AF_INET, (&servaddr.sin_addr), buf, 64);

inet_pton 负责将字符串转为数值格式。
inet_ntop 负责将数值转为字符串格式。
p(presentation) 和 数值n(numeric)。

举例:时间服务器

客户

客户端通过创建 socket,connect 发起连接建立请求。

int main(int argc, char** argv)
{
    int sockfd, n;
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    
    if (argc != 2)
        err_quit("usage <ipaddress>");

    if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)        
        err_sys("socket error");
    
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
        err_sys("inet_pton error");

    if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) < 0)  
        err_sys("connect error");
    
    while( (n = read(sockfd, buf, MAXLINE)) > 0) {
        buf[MAXLINE] = '\0';
        if (fputs(buf, stdout) == EOF)
            err_sys("fpus error");
    }
    if (n < 0)
        err_sys("read error");
}
服务器

服务器端通过创建 socket,bind,listen 完成初始化,通过 accept 完成连接的建立。

int main(int argc, char**argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    time_t ticks;
    
    if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("sockfd error");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8080);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
    
    listen(listenfd, LISTENQ);
    
    while(1) {
        connfd = accept(listenfd, NULL, NULL);
        ticks = time(NULL);
        snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
        write(connfd, buf, strlen(buf));
        close(connfd);
    }
}

参考资料
1、《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff

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

推荐阅读更多精彩内容