使用socket套接字通信
1. socket是什么意思?
n. 插座;插口;窝;穴孔;套接口,套接字
vt. 装上或插入插座
2. 套接字是什么?
套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
非常非常简单的举例说明下:
Socket = Ip address+ TCP/UDP + port
TCP套接字=IP地址+TCP协议类型+端口号
3. c语言函数括号里面是什么?
比如,int abc( int a, int b); 里面的 int a, int b是什么意思?
形式参数。你可以这么理解:
在数学函数 y=2x 中y就是函数名,相当于“abc”;x就是那个括号里面的“形式参数”;当你调用这个函数时,需要将一个具体的值代入给x,这个具体的值叫做实际参数。
形式参数确定了该函数调用需要哪些值,或者说“原料”。
4. 任务
1, 树莓派上运行socket客户端程序,每隔30秒以字符串“ID/时间/温度”形式上报采样温度,其中ID为树莓派的编号,便于服务器端区别是哪个树莓派客户端如“RPI0001/2019-01-05 11:40:30/30.0C”;
2, 通过命令行参数指定服务器IP地址和端口以及间隔采样时间, getopt或getopt_long;
3, 程序放到后台运行(daemon),并通过syslog记录程序的运行出错、调试日志;
4, 程序能够捕捉kill信号正常退出;
获取温度的流程:
5. TCP/IP 的诞生
在今天的基于TCP/IP的互联网诞生之前,能够使用接口通信处理实现互联互通的电脑并不多,而且大部分电脑之间信息的交换并不兼容。后来好几个牛逼哄哄的歪果仁开始捣鼓一些协议,能够让电脑之间进行通信。终于在1974年12月,Bob Kahn和Vinton G.Cerf带领的团队首先制定出了通过详细定义的TCP/IP协议标准。当时作了一个试验,将信息包通过点对点的卫星网络,再通过陆地电缆,再通过卫星网络,再由地面传输,贯串欧洲和美国,经过各种电脑系统,全程9.4万公里竟然没有丢失一个数据位,远距离的可靠数据传输证明了TCP/IP协议的成功。
对于TCP/IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层和数据链路层。
从上面的整个流程中,我们会发现一个TCP的网络链接中求包含一个四元组,即: 源IP、目的IP 和 源端口、目的端口。这就像我们收发快递一样要有发件人和发件人地址、收件人和收件人地址一样。在理解上面的计算机网络通信的工作原理之后,接下来我们就可以更好的理解Linux下的网络socket程序开发了。
socket 编程
在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:“open—write/read—close”模式。
TCP服务器
vim socket_server.c //创建文件夹
rm -rf socket_server.c //删除文件
man socket //查找socket函数的使用
6. socket()函数
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
int socket ( int domain, int type, int protocol );
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:顾名思义,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
7. bind()函数
bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
int bind ( int sockfd, const struct sockaddr * addr, socklen_t addrlen );
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核
addrlen:对应的是地址的长度。
struct sockaddr_in serv_addr ; //定义一个 sockaddr_in类型结构体(包含服务器地址)
memset(&serv_addr, 0, sizeof(serv_addr)); // 清空
serv_addrserv_addr.sin_family = AF_INET; // 协议族
serv_addr.sin_port = htons(LISTEN_PORT);
// 通过调用两个函数 htons() 和 htolnl() 分别用来将端口和IP地址转换成网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有的IP地址
8. listen()函数
listen(listen_fd, BACKLOG);
socket()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,该函数将socket变为被动类型的,等待客户的连接请求。
int listen( int sockfd, int backlog );
sockefd: socket()系统调用创建的要监听的socket描述字
backlog: 相应socket可以在内核里排队的最大连接个数
9. accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
*addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
addrlen: 返回客户端协议地址的长度
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
这时我可以使用 inet_ntoa() 函数将32位整形的IP地址转换成点分十进制字符串格式的IP地址“127.0.0.1”,我们也可以调用ntohs()函数将网络字节序的端口号转换成主机字节序的端口号。
10. connect()函数
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t *addrlen);
在调用connect之前,我们需要先设置服务器端的IP地址和端口等信息到addr中去,参考之前的示例代:
memset(&serv_addr, 0, sizeof(serv_addr)); //清空服务器的
serv_addrserv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
inet_aton( SERVER_IP, &serv_addr.sin_addr );
if( connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
printf("connect to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
return 0;
}
11. 编译socket
gcc socket_server.c
man 函数 // 查找功能
man 2 read // 2表示库函数