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 函数一致,返回值为接收到的字节数。