8.网络编程

socket

socket一般是指socket interface。socket中文被翻译成套接字,是一个不明所以的翻译。socket英文的本意是插座,你买任何一个清水房里面也会有插座,不然这房子没法住——插座是一个房子必须预置的,你只要把插头往里面一插,你就可以使用电器了。socket编程即这个目的:只要有socket,网络编程就如同读写文件一样容易。

socket的概念最早出现于1971年的ARPANET。socket在1982年成为了编程接口,出现于Berkeley Systems Distribution of Unix (BSD 4.1c)——而BSD 4.1a是首个加入TCP/IP协议栈的Unix系统。在20世纪90年代,随着互联网的兴起,socket接口得到了广泛的使用。sockets主要是以“Berkeley sockets”而闻名。

计算机网络的通信实体

在计算机网络上,程序员最在乎的通信不是计算机之间的通信,而是进程与进程通过网络的通信。IP协议的目的,就是通过一个ip地址,能够找到一台主机。而传输层通过端口号,对ip地址进行复用,使得通信的实体变成了进程。即可以通过唯一地确定一个进程。互联网的事实协议就是TCP/IP。传输层的TCP协议能够实现通信双方进程的面向连接的、可靠的、基于字节流的通信——TCP与打电话十分相似。而UDP协议更像写信。

TCP socket

使用TCP socket通信的两个进程并不是对等的。正如前文所说,TCP与打电话十分相似,因而两个进程必定一个是“主叫”一个是“被叫”。被叫一方任何时候都准备接听电话,这种成天准备接电话的人在现实生活中就是电话客服(接线员),称为server,中文翻译为服务器。注意,这里的“服务器”是一个进程,而非硬件服务器。而给客服打电话的人,即是client(客户)了。这种程序被称之为TCP客户/服务器程序。

使用TCP sockets,只要通过一系列操作,客户端和服务器都会获得一个双工的文件描述符:希望发给对方的字节,直接写给该文件描述符即可;希望获得对方发给自己的字节,从这个文件描述符读即可。——复杂的网络编程变成了简单的读写文件!这也是之前所说的“Unix中万物皆文件”。

基本TCP客户/服务器程序所需要的socket函数及TCP连接过程如下图所示。

img

既然TCP与打电话十分相似,其系统调用也是同打电话非常相似的,这种类比特别便于我们记忆以上系统调用及其顺序。

TCP服务器系统调用过程

socket系统调用

想要提供一个电话客服服务,假如使用手机,首先要决定通信协议,比如dsm、cdma、4g,还是5g。在本课程里,我们首先希望选择IPv4,然后选择TCP。

socket系统调用共有三个参数,第一个是最大的协议范围,比如IPv4(AF_INET)、IPv6(AF_INET6);第二个参数是协议类型,字节流的(SOCK_STREAM)、还是数据报等等。对于IPv4,字节流的就是TCP,数据报就是UDP,第三个参数会指出更具体的协议,而对于TCP和UDP就没有更细的划分了,写0就可以了。

man 2 socket中的解释非常详细,并提示我们如果对IPv4感兴趣,应该参考man 7 ip,还详细介绍了如何绑定IP地址和端口号。

bind系统调用

确定好通信协议后,下一步就是要给自己确定一个电话号码,这样客户才能通过这个电话号码打电话过来。IPv4中通过来找到一个进程。因此,bind的目的,就是给服务器进程绑定一下IP地址和端口号,尤其是端口号,一旦一个进程绑定到一个IP地址的某个端口号,其他进程就不能再绑定到该端口号了,以确保操作系统不会把数据发送给多个进程。另外,服务器往往希望在本机所有IP地址的某个端口号上提供服务,因此通常会使用INADDR_ANY,而不是具体的一个IP地址——这也会解决IP地址变动导致问题。IPv4的IP地址是一个32bit(4个字节)的整数,而端口号是一个16bit(2个字节)的整数。由于网络传输一个整数的字节序与本机的表示可能是不同的(大端和小端的问题),所以我们必须通过“host to network”函数来完成本地字节序到网络字节序的转换,htons(host to network short)用于转换16bit的整数;htonl(host to network long)用于转换32bit的整数。在下面例子中,我们选择绑定所有IP地址,我们会从命令参数获取端口号:首先用atoi把字符串变成整数,然后再用htons将其变为网络字节序的16bit整数。

listen系统调用

用listen标记后的socket描述符才能进行accept,第二个参数backlog在不同的Unix中含义是不同的,在Linux 2.2之后,它用来指定等待被accepted的所有完全建立sockets的队列的长度。

accept系统调用

调用accept后,服务器会阻塞,直到客户端用connect连接过来。当TCP连接建立成功后,accept会返回一个标准的文件描述符,这时用读/写文件的方式,即系统调用read/write,来操作这个文件描述符就可以了。

close系统调用

服务器和客户端的通信结束后,close该文件描述符,通信结束。

一个简单的TCP服务器

一个最简单的TCP服务器如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>

main(int ac, char *av[]){
    int tcp_socket;
    struct sockaddr_in addr;
    int fd;
    char buf[512];
    int n;

    tcp_socket  =  socket(AF_INET,  SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(av[1]));
    addr.sin_addr.s_addr = INADDR_ANY;

    if(bind(tcp_socket, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in))==-1){
        perror("cannot bind");
        exit(1);
    }   

    listen(tcp_socket, 1);

    fd=accept(tcp_socket, NULL, NULL);

    n=read(fd, buf, sizeof(buf));
    write(fd, "You said:", 9);
    write(fd, buf, n);  

    close(fd);
}

当我们服务器通过accept等待客户端连接,而客户端成功连接那一刹那后,accept就返回了一个文件描述符fd。对于服务器进程而言,此时fd与一个以读/写方式打开的文件几乎无异——只不过写入fd的数据会发送给客户端,而读fd就会读到客户端发送给服务器端的内容。当然,如果服务器试图读而客户端却不发送数据,这时通信就会“卡”在那里——这就涉及到了应用层协议设计的问题。

运行结果:

首先启动服务器。对于服务器的端口号选择要注意,1024以下(不包括1024)的端口号只有root才能bind。此时服务器在accept阻塞,等待客户端connect。

为了测试我们的TCP服务器,我们还需要一个TCP客户端。这时我们有两个选择:(1)再编写一个客户端——但我们暂时不建议你这么做,因为如果出现了bug或是其他问题,我们很难判断到底是服务器的问题还是客户端的问题;(2)使用一个通用的TCP客户端,比如telnet。

telnet是基于TELNET协议远程登录主机的客户端程序。当然,由于telnet使用明文传送报文,现在几乎没人使用,而被ssh替代了。但telnet本身就是一个标准的TCP客户端,它会把服务器write到socket fd的字节(C语言中的没有字节,字符和字节不做区分,都是8bit)原封不动地在终端显示出来;也会把你在telnet上输入的字符write给服务器。因而,它是一个非常简易地测试服务器的客户端工具。

我们在另一个终端连接一下服务器。

image-20211229220119275

TCP客户端系统调用过程

socket系统调用

同TCP服务器一样,想要打电话首先也得有电话。因此TCP客户端的socket系统调用与TCP服务器是完全一致的。

connect系统调用

客户端想要连接服务器,不需要限定自己的IP地址和端口号,因此TCP客户端的端口号是随机分配的——自然也不需要绑定这一过程。直接通过connect通过服务器的IP地址和端口号连接至服务器。

这里主要注意一点,IPv4的地址是一个32bit的网络字节序的整数,而我们平时都写成点分十进制,比如本机回送地址为“127.0.0.1”,如果用32bit整数表示是:0x7F000001,十进制就是2130706433。为了方便,我们还是推荐使用点分十进制,如“127.0.0.1”,再通过inet_addr库函数将其转换为32bit的网络字节序整数。

connect的调用格式与bind几乎是一样的。但有一个细节非常特殊!就是connect后的文件描述符不是通过返回值获得的,而是connect的第一个参数sockfd就是用于读写的文件描述符。

connect成功后,通过read/write操作文件描述符socket就能与服务器通信了。通信结束后,同close(sockfd)释放连接。

一个简单的TCP客户端

能够正常访问上面编写的简单TCP服务器的客户端代码如下:

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

main(int ac, char *av[]){
    int tcp_socket;
    struct sockaddr_in addr;
    int fd;
    char buf[512];
    int n;

    tcp_socket  =  socket(AF_INET,  SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = htons(atoi(av[2]));
    addr.sin_addr.s_addr = inet_addr(av[1]);

    if(connect(tcp_socket, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in))==-1){
                perror("cannot connect");
                exit(1);
        }

    write(tcp_socket, "from client", 11);

    n=read(tcp_socket, buf, sizeof(buf));
    printf("%.*s\n", n, buf);

    close(tcp_socket);  
}

首先在一个终端上运行上面编写的服务器:

image-20211229220932030

为了防止分不清是谁的输出,换了个终端执行客户端:

image-20211229221029465

应用层协议

当TCP的服务器和客户端建立连接后,尽管双方都可以通过读/写文件的方式与对方通信,但是一方写数据而另一方就需要去读这个这个数据。如果双方同时读,这样大家就卡在那动不了了。应用层协议就是负责这件事:谁先读、谁先写、传输的字符序列(字节序列)的含义是什么。计算机网络协议中,应用层协议是最与我们程序员关系最紧密的。对于某些特殊的网络程序,我们可能还得设计新的协议。HTTP是最有代表性的应用层协议,掌握了它就可以触类旁通其他的应用层协议。下一章我们会深入学习HTTP,和Web服务器的编写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值