Linux Socket通信:一文详解TCP通信API用法

1. 引言

C++自带了socket通信的API,借助这些API可以很方便的进行计算机间的通信。常用的TCP通信基本函数有
socket() , bind() , listen() , accept() , connect() , read() , write() 等。

通常,通信双方会被称为服务端和客户端,服务端可以和多个客户端交流,同时也负责转达客户端之间的信息;客户端之间不能直接进行通信,需要经由服务端转发。

服务端和客户端的API调用情况不一样,分别为
服务端:socket() , bind() , listen() , accept() , read() , write()
服务端:socket() , connect() , read() , write()

下面详细介绍每个函数的作用和用法。

2. socket函数

socket函数在头文件<socket.h>中,作用是向内核申请一个套接字,差不多就是告诉内核我需要网络通信,你给我分配一下。函数原型为:

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

参数介绍:

int af:af表示地址族,即IP地址类型,比如,AF_INET是IPv4 internet协议,AF_INET6是IPv6 internet协议。

int type:该参数用于设置套接字的通信类型,常用的有 SOCK_STREAM(流格式套接字,TCP) 和 SOCK_DGRAM(数据报套接字,UDP)。

int protocol: protocol 表示传输协议类型,大多数情况下,由af和type已经足够确定通信方式了,比如type选择SOCK_STREAM和SOCK_DGRAM之后,系统可以自行推断出protocol参数,但是有少数通信类型包含多种协议,此时计算机就没法进行推断了。

返回参数:文件描述符,一个int型整数。如果成功创建了一个套接字,会返回一个大于0的整数,一般不会是0 1 2,因为一般会外设硬件占用了;如果创建失败,会返回小于0的整数。socket函数有创建失败的可能,因为要及时检查返回值。

socket()函数使用示例:

#include <sys/socket.h>
#include <stdio.h>

int main() {
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0); //IPv4 TCP
    if ( sockfd < 0 ) {
        printf("ERROR: socket creat failed.\n");
    }
}

3. sockaddr_in结构体

上面提到的API中并没有提到sockaddr_in,因为它只是一个结构体,不过却十分重要,socket通信中的IP 端口等信息都由这个结构体来保存。
sockaddr_in定义:

    struct sockaddr_in{
        sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
        in_port_t        sin_port;     //16位的端口号
        struct in_addr  sin_addr;     //32位IP地址
        char            sin_zero[8];  //不使用,一般用0填充
    };

struct in_addr     //sockaddr_in的第三个元素
  {
    in_addr_t s_addr;
  };

可以看到,sockaddr_in由四个元素构成,元素意义具体如下:

  • sa_family_t sin_family:sin_family与socket()函数第一个参数含义一样,表示地址族,而且设置的值也要一样,比如socket()里边是AF_INET,那么sin_family也要设置为AF_INET。sa_family_t其实就是 unsigned short int,占两个字节,不再赘述。
  • in_port_t sin_port:sin_port 为端口号,in_port_t 类型,其实也是 unsigned short int,俩字节。
  • struct in_addr sin_addr:这是一个结构体,in_addr结构体里边只有一个元素,存储的是IP地址,是一个 in_addr_t 型的变量,实际上是一个 unsigned int 。
  • char sin_zero[8]:保留位,不存储信息,一般全部设置为0,占用8个字节。

这个时候就有一个问题,为啥要有8个字节的保留位,不占地方吗?这是为了与通用地址结构 sockaddr 的长度保持一致。后边讲到 bind 函数的时候,你会发现 bind 函数的要求传入的是 sockaddr 结构体,传参的时候还需要将 sockaddr_in 转化为 sockaddr 。那为啥不直接用 sockaddr 结构体呢,因为 sockaddr 用起来不方便,sockaddr 的定义为:

    struct sockaddr{
        sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
        char         sa_data[14];  //IP地址和端口号
    };

sockaddr 结构体将地址,端口都放在 char sa_data[14] 里了,赋值很不方便,所以才 sockaddr_in 等一系列具有针对性的地址结构体,比如 IPv6 的地址结构体为 sockaddr_in6 。而 sockaddr_in 中的8为保留位就是为了在强制转化为 sockaddr 的时候不会丢失字节。

sockaddr_in 结构体的赋值示例:

    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));  //全部设为0
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8000);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

其中,htons 和 inet_addr 函数是转换函数,计算机并不能直接读懂端口 8888 和 IP “127.0.0.1”,需要进行一下转化,具体用法以后再写吧,这篇就不写了。

4. bind函数

bind函数用于绑定服务端套接字和地址,函数原型为:

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

参数详情:

  • int sockfd:这个是前边socket函数创建的套接字。
  • my_addr:地址结构体,类型是通用地址结构体 sockaddr ,需要将输入 sockaddr 等类型进行强制转换。
  • addrlen:地址长度,为 socklen_t 类型,其实就是个 unsigned int,所以这个参数直接用 sizeof(myaddr)即可。
  • 返回值:返回一个整数,0表示正常,-1表示绑定失败。

对bind函数的理解:主要在于如何理解绑定二字,个人理解是将套接字与指定的端口绑定,以后该套接字的信息收发都用这个端口。

使用示例:

int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    if ( ret < 0 ) {
        printf("ERROR: bind failed.\n");
        exit(1);
    }

5. listen函数

listen函数用于监听尝试连接本机的请求,由于服务端套接字已经与某一个端口绑定,listen 实际上就是监听这个端口。此外,由于计算机一次只能处理一个连接请求,当多个连接请求到来的时候,listen 函数将暂不能处理的连接请求放到等待队列中。 listen 函数的原型为:

int listen(int sockfd, int backlog)

参数解释:

  • int sockfd:套接字。
  • int backlog:最大等待队列,如果达到了最大值,新的请求将不再受理。
  • 返回值:0为正常,-1为运行失败。

用法示例:

int ret = listen(sockfd, 10);
    if ( ret < 0 ) {
        printf("ERROR: listen failed.\n");
        exit(1);
    }

6. accept函数

accept()函数用于接收客户端的连接请求,函数原型为:

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

参数解释:

  • sockfd:服务端套接字。
  • addr:客户端的地址信息,accept 将接收到的客户端地址存入 addr 中。
  • addrlen:地址长度。
  • 返回值:accept 的返回值是一个新的套接字,该套接字就专用于这两个计算机之间的通信;每一次 accpet 都会返回一个新的套接字。

accept函数会返回一个新的套接字,这样就与客户端建立了一条通信“管道”。那么老的套接字呢,当然是在继续监听端口。

使用示例:

struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len = sizeof(clnt_addr);  
int clnt_sock = accept(sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_len); //接受客户端请求

注意后边两个参数都是指针。

7. connect函数

客户端在套接字之后,就可以调用 connect 函数请求连接服务端,函数原型为:

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

参数解释:

  • sockfd:客户端套接字。
  • addr:请求连接的地址,要与服务端一致。
  • addrlen:地址长度。
  • 返回值:0为正常,-1为运行失败。

8. write函数

当服务端与客户端连接成功之后,可以通过新的套接字进行通信。服务端与客户端之间的消息发送和读取,可以通过 write() 和 read() 函数,这与读写普通文件使用的API是一样的。虽然表面上一样,但是在调用内核函数的时候,内核会判断该文件描述符是通信套接字,然后调用对应的内核函数。

使用write 函数发送消息,函数原型为:

int write(int sock_fd, const void* buffer, size_t len)

参数解释:

  • sockfd:通信套接字,是 accept 返回的新的套接字。
  • buffer:是一个 void 型的指针,指向了待发送消息的地址,发送消息的类型是无所谓的,找到所在地址就行。
  • len:消息字节数,buffer 指向信息的首地址,还需要知道长度才能完整发送。
  • 返回值:返回发送的字节数,如果是负数,表示发送失败。

9. read函数

read 函数用来接收消息。

int read(int sock_fd, const void* buffer, size_t len)

参数意思与 write 函数一致,返回值为接收到的字节数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值