三大通识知识:进程,线程,网络(五)

三大通识知识(一):进程,线程,网络之间的关系
三大通识知识(二):进程实现原理
三大通识知识(三):线程实现原理
三大通识知识(四):网络
三大通识知识(五):服务器搭建
进程,线程,网络视频连接

前言

上一节课,我们讲了网络的几个常见问题:
什么是网络,为什么叫TCP/IP,什么是OSI模型,如何记住OSI模型,为什么TCP是可靠的,UDP是不可靠的,为什么TCP是面向连接的,UDP是无连接的?
这一节课,我们讲如何搭建TCP服务器和客户端。

如何搭建TCP服务器

TCP/IP协议已经发展了接近40年,所以已经非常成熟了,这种成熟不仅表现在它的稳定性上,而且表现在它知识的传播上,使用的简单性上,就连编程实现也有成熟的模型,这个模型就叫TCP编程模型:
在这里插入图片描述
我们搭建TCP服务器,就根据这个模型来写代码,它已经很清楚的告诉我们第一步做什么,第二步做什么。

服务器代码

好,接下来我们就按照这个来写服务器程序:

pi@xiajiashan:~/$ cat -n server.c 
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <sys/types.h>          /* See NOTES */
     4  #include <sys/socket.h>
     5  #include <netinet/in.h>
     6  #include <arpa/inet.h>
     7  #include <string.h>
     8  //int socket(int domain, int type, int protocol);
     9  int main()
    10  {
    11      int fd,result;
    12      fd = socket(AF_INET,SOCK_STREAM,0);
    13      printf("fd=%d\n",fd);
    14      if(fd==-1){
    15         perror("socket函数调用失败...");
    16         exit(-1);
    17      }
    18      //step2----------------
    19      //       int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    20      struct sockaddr_in  server;//存放服务器的IP地址
    21      server.sin_family = AF_INET;
    22      server.sin_port = htons(8990);//把主机字节序转成网络字节序
    23      server.sin_addr.s_addr = inet_addr("192.168.137.243");
    24
    25      result = bind(fd,(struct sockaddr*)&server,sizeof(struct sockaddr));
    26      printf("bind result=%d\n",result);
    27      if(result==-1)
    28      {
    29         perror("bind failed....");
    30         close(fd);
    31         exit(-1);
    32      }
    33      //step3--------
    34      result =  listen(fd, 10);
    35      printf("listen result=%d\n",result);
    36      if(result==-1)
    37      {
    38         perror("listen failed....");
    39         close(fd);
    40         exit(-1);
    41      }
    42      //step4----------------------
    43      struct sockaddr_in  client;//存放客户端的IP地址和端口号
    44      memset((void*)&client,0,sizeof(client));
    45      socklen_t addrlen=0;
    46      int client_fd;
    47      int size;
    48      char buff[1000]="";
    49      while(1){
    50            puts("accept....");
    51            //int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    52            client_fd = accept(fd,(struct sockaddr*)&client,&addrlen);
    53            printf("client_fd = %d,ip = %s, port = %d\n",client_fd,inet_ntoa(client.sin_addr),&addrlen);
    54            while(1){
    55                 size = read(client_fd,buff,1000);
    56                 printf("从客户端读到 %d 字节:%s\n",size,buff);
    57                 if(size==0) {
    58                    perror("客户端已经关闭...");
    59                    break;
    60                 }
    61                 memset((void*)buff,0,1000);
    62                 //write()
    63            }
    64      }
    65      close(fd);
    66  }
pi@xiajiashan:~/$ 

客户端代码

客户端的代码呢,我们也在ubuntu 18.04这个系统上测试,也就是说我们的服务器和客户端都跑在同一个电脑上,不是两台电脑通信,这其实完全不影响我们编码,你把客户端拿到另一台电脑(linux系统),只需要修改一下服务器IP地址就可以照样运行了。
客户端代码如下:

pi@xiajiashan:~/pi-c$ cat -n client_tcp.c 
     1  #include <stdio.h>
     2  #include <sys/types.h>          /* See NOTES */
     3  #include <sys/socket.h>
     4  #include <stdlib.h>
     5  #include <fcntl.h>
     6  #include <unistd.h>
     7  #include <netinet/in.h>
     8  #include <arpa/inet.h>
     9  #include <stdlib.h>
    10  #include <string.h>
    11  //int socket(int domain, int type, int protocol);
    12  #define SERVER_IP "192.168.137.243"
    13  #define SERVER_PORT 8990
    14  int main(void)
    15  {
    16    int fd;
    17    //第一步:创建套接字
    18    fd = socket(AF_INET,SOCK_STREAM,0);//使用ipv4地址族
    19    printf("fd = %d\n",fd);
    20    if(fd==-1)
    21    {
    22       perror("socket failed...");
    23       exit(-1);
    24    }
    25    //第二步:链接到服务器
    26    //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    27    int retval;
    28    struct sockaddr_in  serveraddr;//设置服务器IP地址
    29    serveraddr.sin_family = AF_INET;//IPV4
    30    serveraddr.sin_port = htons(SERVER_PORT);//把主机地址转为网络地址
    31    //  in_addr_t inet_addr(const char *cp);
    32    serveraddr.sin_addr.s_addr = inet_addr(SERVER_IP);
    33    retval = connect(fd,(struct sockaddr*)&serveraddr, 16);
    34    printf("retval = %d\n",retval);
    35    if(retval==-1)
    36    {
    37      perror("connect failed...");
    38      close(fd);
    39      exit(-1);
    40    }else{
    41      printf("连接到服务器(%s:%d)成功...\n",SERVER_IP,SERVER_PORT);
    42    }
    43    char buf[100]="";
    44    while(1)
    45    {
    46       printf("请输入要发送给服务器的数据(end结束):");
    47       fgets(buf,100,stdin);//从键盘获取100个字节存放到buf中
    48       retval = write(fd,buf,strlen(buf)-1);
    49       if(strncmp(buf,"end",3)==0) break;
    50       memset(buf,0,100);
    51    }
    52    close(fd);
    53    return 0;
    54  }
    55    
pi@xiajiashan:~/pi-c$ 

联调结果:

在这里插入图片描述

代码赏析

各种编程,为什么叫语言,比如C语言,java语言,python语言。。。。
其实,计算机就是一个族类,它们沟通就需要这种机器语言,就像我们人类一样。
我一直认为,跟机器打交道是最容易的,而跟人打交道是最难的;
可是,有些人却觉得跟人打交道是最容易的,而跟机器打交道是最难的;真羡慕这种人!所以我适合从事计算机方面的职业,我觉得不善交际的人,最好就做跟机器打交道的事情,这里就有一个矛盾了,这个矛盾跟这句有哲理性的话有关——做自己不擅长的,才可以提高自己!如果我们不善交际,就做不需要交际的事情,是不是我们的语言能力会下降得更快?我现在回想自己做研发时候,可以整天不说一句话,真有点可怕,我曾经也听我一个学生(女孩子),她说她的leader可以整天不说一句话!
我觉得,这是一种性格障碍,需要想办法改变。。。
好了,闲话不多说,我们进入正题。
我们来一句句的分析上面的代码,先分析服务器部分代码:

服务器代码解析

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h> /* See NOTES */
4 #include <sys/socket.h> //调用socket函数需要
5 #include <netinet/in.h> //调用inet_addr函数需要
6 #include <arpa/inet.h> //调用inet_addr函数需要
7 #include <string.h> //调用memset,strlen函数需要
8 //int socket(int domain, int type, int protocol); socket函数原型
9 int main()
10 {
11 int fd,result; //定义变量
12 fd = socket(AF_INET,SOCK_STREAM,0);
socket这个函数的功能是创建套接字,什么是套接字呢?socket的英文是"插座,插槽"的意思,就像这样:
在这里插入图片描述
为什么叫插槽呢,它把服务器比作了插线板,客户端比作电源插座。
socket函数原型如下:
int socket(int domain, int type, int protocol);
第一个参数domain,这个单词是“领域”,“域名”的意思,在这里指网络覆盖的区域,我们传的参数是AF_INET,是什么意思呢?
在这里插入图片描述
这里AF表示地址家族 Address Family,INET其实就是因特网的意思internet。
那AF_INET究竟有指什么呢?它指IPV4,这个IPV4又不懂了,其实我们很熟悉了,只不过这里用专业术语来描述的。我们一天到晚上网用的IP地址192.168.1.100就是一个IPV4,首先是IP地址,然后V是version版本的意思,4就是表示4个字节,你看192,168,1,100就是四个字节,这是TCP/IP协议规律的,用字符表示IP地址的方法,也叫点分制。
第二个参数type,表示协议类型,这个就比较好理解了,我们一般就两种类型,一种TCP,另一种UDP,如果是TCP传的参数就是SOCK_STREAM,如果是UDP,传的参数就是SOCK_DRAM,这里stream是数据流的意思,DRAM是数据报文的意思是datagram。
第三个参数protocol,表示协议,这里不做解释,一般填0,也不要问为什么(因为我没有找到权威说法)。
socket函数的返回值,有些函数的返回值很简单,仅仅代表这个函数是否调用成功,这种返回值意义不是很大,很多时候可以忽略它,但是有些函数的返回值不仅代表函数调用成功,还代码非常重要的意义,比如我们这里的socket函数,它的返回值就代码一个文件描述符,linux系统中,是用文件来管理各种设备的,什么意思呢,比如无线网口,驱动工程师就把它映射成了一个文件,这样我们应用工程师就可以像操作文件一样打开,关闭,读写。这里也是一样,返回值就代码一个设备文件,在linux系统中,文件标识符是一个整形变量。所以,我们这里使用fd这个整形变量来接收它。
13 printf(“fd=%d\n”,fd);//以10进制方式打印出来
14 if(fd==-1){
//如果fd等于-1则表示函数执行失败,perror是打印错误原因,p就是printf的意思,这个函数有时候非常有价值,它可以告诉我们出错的原因,很多时候这个原因非常靠谱,我们就知道是哪里的问题。
15 perror(“socket函数调用失败…”);
16 exit(-1);
exit函数,是直接退出进程,exit这个字样我们应该经常看到
在这里插入图片描述
就是程序不往下走了,退出了,结束了。
17 }
好了,我们第一步就完成了,根据TCP编程模型,就该到第二步bind了。
18 //step2----------------
bind这个函数是绑定的意思,什么是绑定呢,你看第一步socket是创建一个套接字,这个套接字其实还没有实际意义,你看还没有跟任何IP地址关联嘛,所以我们要把这个套接字(socket的返回值)跟我们电脑的IP地址关联起来,那么怎么知道我们电脑的IP地址呢,那就要用到ifconfig命令了,我们一般通过这个命令查看电脑的网卡地址,就像这样:
在这里插入图片描述
好了,知道了电脑的IP地址,接下来就要通过bind函数绑定了。

19 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind函数的第一个参数,就是我们要把IP地址绑定到哪个套接字上去,当然是socket的返回值喽,所以这第一个参数我们就应该传fd(socket函数返回值)。
bind函数的第二个参数,是一个结构体,这个结构体用来存放我们服务器的IP地址和端口号,好了,什么是端口号呢?

什么是端口号

其实,在解释端口号前,我们说说什么是IP地址,其实很多人不一定真正理解。我们先来思考一个问题,比如电脑A要跟电脑B聊天,假设通过QQ,而两台电脑上也安装了微信。我要问的是:A发生hello给B,这个hello为什么被B电脑的QQ接收到了,为什么微信没有收到?有人说,这还不简单吗?因为A是QQ发的啊,当然被B的QQ收到啊!你再往更深层次思考,你要想象这个hello,world是一个字节流,它在网线中流动,从A电脑流出,经过了很多曲折,终于到了B电脑的网线,B电脑的CPU把这个数据流读出来,为什么就给了QQ,而没有给微信?我们来看一张图:
在这里插入图片描述
首先,我们应该有一点共识,就是每一个IP地址对应一个电脑(当然我们现在的电脑都有两个网卡,一个有线,一个无线,我们这里只考虑一张网卡),但是端口号就有很多了,我们电脑上的所有网络程序,都有一个唯一的端口号,也就是说:
IP地址对应某天电脑
端口号对应电脑中的某一个软件(应用程序)
这样因为我们A发送的时候,hello是送到B电脑的8990号端口的,所有B电脑不会把它给8880端口。
好了,有了这个铺垫,我们接下来就好讲了。
20 struct sockaddr_in server;//存放服务器的IP地址
上面的代码时定义一个结构体变量,取名叫server
21 server.sin_family = AF_INET;
给这个要绑定的地址规定是IPV4,也就是4个字节的IP地址。
22 server.sin_port = htons(8990);//把主机字节序转成网络字节序
设置端口号,这里有一个函数htons,这个函数是把主机地址转成网络地址,所有这个名字h是host,主机的意思;
to就是转换的意思,n是网络的意思,s是short,表示端口号是一个short类型,短整形,16位。

为什么要把主机地址转成网络地址

首先什么是主机地址,什么是网络地址,发现这些知识真会把初学者搞死了,其实如果你可以放得开,完全可以不管这些,按照这个步骤,模板直接用就行了。不用重复造轮子,可是,不知为何,有些人就是喜欢去探索未知,像搞清楚每一个不确定,不知道的点,我很喜欢这样的人,只有这样的人才适合做研发。万一轮子不给我们用了呢?不过,我们老师应该高清楚,因为我们跟工程师不一样,工程师只要把东西做出来就行了,至于有些东西是怎么发生的,可以不管。而,我们之所以要搞清楚,是因为我们的学生去工作,也不知道哪些学生是不需要管,哪些学生是要去搞清楚一些原理细节的。所以,我们都得讲。
好了,这个主机地址,网络地址,涉及到一个知识点——字节序。
在这里插入图片描述
我们先不要看文字,如果文字大家能理解,就不至于要图。图是最好的展现方式。
这个图的意思是,有一个数:0x01020304,这是一个16进制数,32位,4个字节,其中01是最高,04是最低位字节。
这个数如何存放到计算机内存中?根据不同的cpu来决定的。比如intel的cpu是这样存放的:
01存放在低地址,04存放在高地址,呵呵,什么是高地址,什么是低地址?
图中,1000就是低地址,1003就是高地址,这种存放有一个名称——大端字节序
相反,像arm的cpu,是小端字节序,它是04存放在低地址,01存放在高地址。
图中,还有一个标志很多人不理解,MSB和LSB,MSB是最重要,LSB是不重要,这个怎么理解呢?
其实,大白话比文绉绉更号,文绉绉是一般专家为了显示自己说话专业,想出来的一些专业名词,真的没有必要。
这里的MSB,LSB你可以这样理解:
比如你借了别人101块钱,你说哪个1重要呢,当然是左边的1啊,你可以把右边的那个1抹掉,但是不能把左边的1抹掉,就这么简单理解。
我们解释了字节序,但是还是没有解释为什么要把主机字节序转成网络字节序,这个问题是这样的:
TCP/IP协议规定,所有在网络上传输的数据,一律以大端字节序存放,不管你是CPU,所以,linux就有一些函数,帮我们把8990转成网络字节序。
这里转发分两种情况,如果是发送到网络,需要把主机转成网络,如果是从网络都进来,则需要把网络转成主机,调用的函数就是ntohs。

23 server.sin_addr.s_addr = inet_addr(“192.168.137.243”);
这里是把字符串表示的IP地址转成结构体能接受的类型s_addr。
24
25 result = bind(fd,(struct sockaddr*)&server,sizeof(struct sockaddr));
第三个参数,就是告诉bind这个函数,我们传进去的结构体有多少字节。
26 printf(“bind result=%d\n”,result);
打印bind函数的返回值,这个返回值意义不是很大,仅仅表示函数调用是否成功
27 if(result==-1)
28 {
29 perror(“bind failed…”);
很好的方法,如果bind失败,可以告诉我们失败原因,很靠谱
30 close(fd);
如果bind失败,应该把fd关掉,其实fd就代表一个文件,socket把这个文件成功打开,我们应该关掉,再退出程序。
31 exit(-1);
退出程序
32 }
33 //step3--------
根据编程模型,我们到了第三步,侦听。
34 result = listen(fd, 10);
侦听,非常好理解,就像有客人来我们家,我们到门口等候,那么第二个参数10,是什么意思呢,这个10是侦听队列长度,就是说一次性可以接待10个人,你可以理解你是开车到村口接客人,这个车一次只能坐10个人,但是不代表你家里只能坐10个人,一批批接回来,可以100个人。
35 printf(“listen result=%d\n”,result);
36 if(result==-1)
37 {
38 perror(“listen failed…”);
39 close(fd);
40 exit(-1);
41 }
上面是判断侦听是否正常
42 //step4----------------------
已经到了第四步,接收客户端连接请求了。
43 struct sockaddr_in client;//存放客户端的IP地址和端口号
为了接收客户端请求,我们需要定义一个客户端的结构体,用来存放客户端信息,你可以理解成客人来你家,带来了礼物,你需要准备一个账本,或者叫礼簿,登记客人信息。
44 memset((void*)&client,0,sizeof(client));
把箱子打扫干净。
45 socklen_t addrlen=0;
用来存放客人信息有多长,也就是客人的名字,电话号码——这里对应IP地址和端口号。
46 int client_fd;
47 int size;
48 char buff[1000]="";
这个buff是用来存放客人送来的礼品,比如客人送来了大米,面粉,水果等等。。。
49 while(1){
50 puts(“accept…”);
51 //int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);
52 client_fd = accept(fd,(struct sockaddr
)&client,&addrlen);
好了,客人所有东西,包括客人姓名,礼物都由accept这个函数来接收了,这个函数有一个特点,如果没有客人来,它不往下走,一直在村口等。那么如果有客人来了,客人的名字存放在结构体client中,这个名字其实对应IP地址和端口号。

53 printf(“client_fd = %d,ip = %s, port = %d\n”,client_fd,inet_ntoa(client.sin_addr),&addrlen);
把客人信息打印出来,方便直观查看。
54 while(1){
while 1表示一直要跟客户通信,不退出,反复接收信息。
55 size = read(client_fd,buff,1000);
读取客户端信息,注意,这里是通过accept的返回值来读取的,accept的返回值非常重要,不能忽略,我们接下来就是通过这个返回值读取客户端连接之后,要发给我们的信息。这个返回值也是文件描述符,它代表客户端,而socket返回值代表服务器。这个read函数的第一个参数就传代表客户端的描述符,也就是accept返回值,第二个参数就是传用来存放客户端数据的数组,第三个参数,就是告诉read函数,我用来接收信息的容量有1000个字节。
而read的返回值也很重要,代表实际读到的字节数。
56 printf(“从客户端读到 %d 字节:%s\n”,size,buff);
打印读取到的内容和长度信息。
57 if(size==0) {
什么适合读到的字节数为0呢,这是一个经验值,我们发现如果客户端异常退出,回值断网了,关闭了,终止客户端退出了,此时服务器read返回值为0,这样就给我们一个信号,我们服务器只要读到的字节数为0,则表示客户端已经断开了。
58 perror(“客户端已经关闭…”);
59 break;
60 }
61 memset((void*)buff,0,1000);
数据打印出来后,我们把buff清空,方便第二次读数据。
62 //write()
63 }
64 }
65 close(fd);
最好关闭服务器的文件描述符。
66 }
pi@xiajiashan:~/$

客户端代码解析

服务器代码理解了,客户端代码就好理解了
1 #include <stdio.h>
2 #include <sys/types.h> /* See NOTES */
3 #include <sys/socket.h>
4 #include <stdlib.h>
5 #include <fcntl.h>
6 #include <unistd.h>
7 #include <netinet/in.h>
8 #include <arpa/inet.h>
9 #include <stdlib.h>
10 #include <string.h>
11 //int socket(int domain, int type, int protocol);
12 #define SERVER_IP “192.168.137.243”
上面代码是一个宏定义,因为服务器代码经常变动,所有在客户端里面把服务器的IP地址做成了宏,后面服务器IP变了,只需要改变这里就行了。
13 #define SERVER_PORT 8990
端口号也同理
14 int main(void)
15 {
16 int fd;
17 //第一步:创建套接字
这一步跟服务器一样,客户端也需要创建一个套接字,而且参数跟服务器一样
18 fd = socket(AF_INET,SOCK_STREAM,0);//使用ipv4地址族
但是这个返回值代表的不是服务器,它代表客户端,此时你可以想象这个程序(客户端程序)运行在另一台电脑,就好理解了。
19 printf(“fd = %d\n”,fd);
20 if(fd==-1)
21 {
22 perror(“socket failed…”);
23 exit(-1);
24 }
25 //第二步:链接到服务器
这一步是客户端的关键地方,它告诉客户端,你要连接的目的地(服务器IP和端口号是多少)
26 //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
27 int retval;
28 struct sockaddr_in serveraddr;//设置服务器IP地址
29 serveraddr.sin_family = AF_INET;//IPV4
30 serveraddr.sin_port = htons(SERVER_PORT);//把主机地址转为网络地址
31 // in_addr_t inet_addr(const char cp);
32 serveraddr.sin_addr.s_addr = inet_addr(SERVER_IP);
服务器的IP地址和端口号通过结构体serveraddr来存放
33 retval = connect(fd,(struct sockaddr
)&serveraddr, 16);
connect函数就会去连接结构体serveraddr所指定的IP和端口号
34 printf(“retval = %d\n”,retval);
35 if(retval==-1)
36 {
37 perror(“connect failed…”);
38 close(fd);
39 exit(-1);
40 }else{
41 printf(“连接到服务器(%s:%d)成功…\n”,SERVER_IP,SERVER_PORT);
42 }
43 char buf[100]="";
44 while(1)
45 {
46 printf(“请输入要发送给服务器的数据(end结束):”);
47 fgets(buf,100,stdin);//从键盘获取100个字节存放到buf中
这个函数是从键盘获取录入的字符,并且存放到buf里面最多可以存放100个,这个函数有两个特点:
1)这个函数会阻塞,上面意思呢,就是如果你不按回车,它就不往下走,直到你输入了100个字符;
2)如果你输入1个字符,然后按下回车,那么这个函数解除阻塞状态,往下执行。
48 retval = write(fd,buf,strlen(buf)-1);
wirte函数,是把从键盘获取到的字符串,发到网络上去,其中strlen是计算buf里面有效的字符串,什么是有效的字符串呢,因为buf其实可以存放100个字符,如果你从键盘只录入一个hello,那么有效字符是5个,这样就只发5个。
49 if(strncmp(buf,“end”,3)==0) break;
这里做了一个结束标志,什么意思呢,就是如果你从键盘输入end然后,回车,那么就跳出循环。
50 memset(buf,0,100);
最后把buf打扫干净,清零。
51 }
52 close(fd);
关闭客户端的套接字。
53 return 0;
54 }
55

END

好了,足足写了2个多小时,字数13831字节,我在想,为什么我要写这么详细?已经深夜0:12分了。
因为我已经开始做这件事了,我想做得更完美!觉得本文对你有帮助,就收藏吧!

三大通识知识(一) :进程,线程,网络之间的关系
三大通识知识(二):进程实现原理
三大通识知识(三):线程实现原理
三大通识知识(四):网络
大通识知识(五):服务器搭建
想通过视频学习的,可以点击下面链接!
进程,线程,网络视频连接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下家山

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值