实际上,这一节的标题应该叫《创建套接字》,如果搞个这样的标题,未免也太学术了,不直观,还容易被群殴。
在上一讲中,我们只讲了“插座”它有一个地址叫“插座地址”,即套接字地址,可是我们一直没说清楚在进程中这个插座是怎么来的。插座并不是一开始就有的,而是需要创建的。
1. 安装 socket
请自动的把 socket 翻译成“插座”。
使用函数 socket 就可以办到,这个函数非常简单:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Linux 的一切皆文件的精神在这里又一次体现,socket 返回的是一个描述符,也就是说,你的进程,拿着这个描述符,就能找到插座了。下面看看几个参数。
(1) domain
你学《计算机网络》的时候知道网络有 7 层模型是吧,但是那只是一个理论,具体是什么样的网络那还得看人家程序员怎么写了,比如你自己按照 7 层模型实现了一个网络通信的框架,可能你取个名字叫 IPv8 是吧,完全可以。
所以 domain 就是告诉操作系统,要用哪家写的网络协议。domain 有很多值,只列举两个:
- AF_INET: 表示使用 IPv4 协议
- AF_INET6:表示使用 IPv6 协议
如果未来你自己搞了个 IPv8,那 domain 就可以设置成 AF_INET8 啦。
所以,以后我们编程,选哪个,你自己清楚吧。
(2) type
type 字段表示通信的交流方式,在生活中就表示你是要发短信,还是要打电话?
它有很值,目前我们先了解两个:
- SOCK_STREAM:有序的,可靠的,双向的基于连接的字节流。
- SOCK_DGRAM:无连接的,不可靠的,固定长度的报文。
总之别喷我,man 手册上原文就是这么写的,如果计算机网络掌握的不是太好,懵圈了正常。总之,SOCK_STREAM 对应于生活中的打电话,而 SOCK_DGRAM 对应于生活中的发短信。
打电话是有序的,可靠的的吧,双向的吧。发短信肯定是不可靠的吧,发个短信,可能因为信号的原因,对方没收到。这里就不细讲了。我们的目的是写代码是吧?
(3) protocol
用哪家的造的网络协议栈确定了,用哪种通信方式确定了,最后就是使用哪种具体的协议来实现这种通信方式了。
比如你要打电话,具体的来说,你要怎么打电话?用中国移动的还是中国联通的?
对于发短信也是一样,你是要用 QQ 发呢还是用微信发?
在网络通信里,打电话发短信也要选一种方法,具体就是靠 protocol 了。
通常来说 protocol 如果设置为 0,就表示使用一种默认的方式了。比如你要打电话,默认情况下你就用中国移动的。发短信,默认你就用 QQ。
SOCK_STREAM 默认使用的协议是 TCP,而 SOCK_DGRAM 默认使用的协议是 UDP。所以在后面的编程中,我们都默认将 protocal 设置成 0,不用管。什么时候你成了网编编程高手了,再自己慢慢研究吧。
protocol 的其它可选值,就不列举了。
2. 将 socket 与套接字地址绑定
前面的套接字地址你也知道是干嘛使的了,插座我们也有了,接下来,就是给插座取名字了。毕竟一个进程中你可以创建好多好多个插座是吧,如果不取名字那就外面的进程就分不清谁谁谁了。
所以,如果要想外面的进程能够找到你的插座和你通信,就必须要将插座和套接字地址进行绑定。
图1 两个进程间通信
函数 bind 可以将套接字地址与 socket 进行绑定:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
我知道你又懵圈了,这上面的套接字地址的类型是 struct sockaddr 而不是我们前面学过的 struct sockaddr_in 啊?你的疑问是正常的,因为 bind 函数不仅仅是设计给 IPv4 用的,也会给 IPv6 地址,所以 struct sockaddr 就有点像一个抽象类。
因此,你在构造完套接字地址后,直接把 struct sockaddr_in 的地址强转成 struct sockaddr 指针就行了。
参数 addrlen 是参数 addr 指向的套接字地址的结构体大小。
还需要注意的地方:如果你并不想让别的进程连接到你的进程,你并不需要将 socket 绑定到套接字地址上。需要绑定套接字地址的进程,通常就是可以让别人找到你并连接上你,所以这种进程通常都称之为服务器进程。
3. 实验
这就是一段没什么用的代码,你可以不用运行(可以通过编译的),这里就是演示一下,怎么创建套接字,怎么将套接字地址绑定上去。
^_^ 这可是你的第一个网络程序。
// mysocket.c
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
int main() {
int ret;
struct sockaddr_in addr;
struct sockaddr_in test_addr;
socklen_t test_addrlen;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
// 这个 ip 地址一定要写你机器的配置的 ip 地址,不然人家怎么找到你呢?
addr.sin_addr.s_addr = inet_addr("192.168.80.130");
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) perror("socket");
ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) perror("bind");
// 函数 getsockname 可以用来查询当前插座绑定的套接字地址。
test_addrlen = sizeof(test_addr);
ret = getsockname(sockfd, (struct sockaddr*)&test_addr, &test_addrlen);
if (ret < 0) perror("getsockname");
// 打印绑定的地址
printf("ip: %s, port: %d\n", inet_ntoa(test_addr.sin_addr), ntohs(test_addr.sin_port));
close(sockfd);
return 0;
}
实际上,有时候你的机器 IP 地址也会变,那怎么办呢?有一个通用地址,叫 INADDR_ANY,你可以把它赋值给 in_addr 结构:
addr.sin_addr.s_addr = htonl(INADDR_ANY);
这样一来,就可以绑定你机器上实际那个可用的 IP 地址啦。
4. 总结
- 使用 socket 函数创建一个插座
- 使用 bind 函数将套接字地址绑定到插座上