Linux基础 ——“网络编程” 了解客户和服务器之间的故事

文章目录

  • Linux网络编程篇
  • 一、网络编程的概念:
    • 1.1 TCP/UDP对比
    • 1.2 端口号作用
  • 二、字节序:
    • 字节序
    • 文件描述符
  • 三、Socket 编程步骤
  • 四、Linux提供的API简析
    • 服务器端
    • 1. 连接协议API:
    • 2.IP地址和端口号 API:
    • 3.地址转变API:
    • 4.监听 API :
    • 5.连接 API :
    • 6.数据收发 API:
    • 7.数据收发常用第二套API
    • 8.客户端的connect函数
  • 五、socket服务端代码实现:
  • 六、socket客户端代码实现
  • 七、实现双方聊天
    • 服务端代码:
    • 客户端代码:

Linux网络编程篇

一、网络编程的概念:

网络:

  1. 地址

    1. IP地址
    2. 端口号
  2. 数据

    1. 协议(http,tcp, udp)

Socket 套接字
1.TCP : 面向连接 如:A 打电话 B (可靠)
2.UDP: 面向报文 如:A 发短信给 B 数据量大 (不可靠)

1.1 TCP/UDP对比

  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前 不需 要建立连接
  1. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  1. TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP电话,实时视频会议等)
  1. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  1. TCP首部开销20字节;UDP的首部开销小,只有8个字节
  1. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

1.2 端口号作用

一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、 SMTP服务等
这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP地址与网络服务的关系是一对多的关系。
实际上是通过“IP地址+端口号”来区 分不同的服务的。端口提供了一种访问通
端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69

二、字节序:

字节序

2.1 字节序概念:
字节序是指多字节数据在计算机内存中存储或者网络传输时各自字节的存储顺序。

2.2 常见序:

  • Little endian(小端字节序) :将低序字节存储在起始地址
  • Big endian (大端字节序):将高序字节存储在起始地址

文件描述符

我们知道在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4…

Linux内核对所有打开的文件有一个文件描述符表格,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表这个数组的下标,数组的内容就是指向一个个打开的文件的指针。

Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第1张图片

三、Socket 编程步骤

网络编程场景模拟:

假如我是 客户端 ,我正要 去朋友家(服务器端)不知道他家具体位置,我的面前有5个楼,我要去他家怎么办呢?

正在这时候,我的朋友突然喊我,他用的 汉语(TCP/UDP) 叫我名字,他说到:我在 XX号楼(IP地址)XX号房间(端口号),我就很明确我要去访问的地方 。他就在房间里 等待我敲门(监听) ,然后我俩就 问他玩游戏吗(请求数据) ,他回到,上号!(回应数据),玩完后,我离开离他家(结束连接),我走后他也去上班了。

Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第2张图片

1.socket ():创建套接字
2.bind() :为套接字添加信息(IP地址和端口号)
3.listen() ; 监听网络连接
4.accept():监听到有客户端接入,接受一个连接
5. 数据交互
6.close() : 关闭套接字,断开连接。

四、Linux提供的API简析

服务器端

1. 连接协议API:

   int socket(int domain ,int type ,int protocol);

参数说明:

参数1:domain

domain : 指明所使用的协议族
通常为AF_INET,表示互联网协议族(TCP/IP协议族);

AF_INET IPv4 因特网域
AF_INET IPV6 因特网域
AF_UNIX Unix域
AF_ROUTE 路由器套接字
AF_KEY 密钥套接字
AF_UNSPEC 未指定

参数2:type

type: 指定socket的类型:

  • SOCK_STREAM : 流式套接字提供可靠的,面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
  • SOCK_DGRAM
    :数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠,无差错的。它使用数据报协议UDP
  • SOCK_RAW : 允许程序使用底层协议,原始套接字允许对底承协议如IPICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。

参数3:protocol

protocol :通常赋值“0 ”
0 选择 type 类型对应的默认协议

IPPROTO_TCP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

2.IP地址和端口号 API:

地址准备好:

bind()函数:IP号端口号相应描述字赋值函数

#include
#include

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

功能: 用于绑定IP地址和端口号到socketfd

参数说明:

sockfd : 是一个socket描述符
addr :是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同

//ipv4 对应的是:
struct  sckaddr{
        unisgned short     as_family;   //协议族
        char                        sa_data[14];  //IP+端口
}

同等替换上面代码:

struct   scockaddr_in {
       sa_family_t     sin_family;   //协议族
       in_port_t         sin_port;       //端口号
       struct    in_addr   sin_addr;    //IP地址结构体
       unisgned   char   sin_zero[8]; 
      //填充,没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换
};

3.地址转变API:

int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);

把字符串形式的“192.168.1.123”转为网络能识别的格式

char* inet_ntoa(struct in_addr inaddr);

把网络格式的IP地址转化为字符串形式

4.监听 API :

listen()函数 : 监听设置函数

#include 
#include 

int listen(int sockfd, int backlog);

功能:

  1. 设置能处理的最大连接数listen()并未开始接受连线,只是设置sockect的listen模式listen函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后**响应该连接请求,**并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。
  2. 主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听)规定内核为相应套接字排队的最大连接数
  3. 内核为任何一个给定监听套接字维护两个队列:
    1. 未完成连接队列,每个这样的SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于 SYN_REVD状态
    2. 已完成连接队列,每个已完成 TCP三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED状态;

参数说明:

sockfd: sockfd是socket系传调用返回的服务器端socket描述符

backlog: backlog指定在请求队列中允许的最大请求数

5.连接 API :

accept()函数

#include 
#include 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能

accept 函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠

参数

sockfd: sockfd是socket系统调用返回的服务器端socket 描述符,是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在

addr: 用来返回已连接的对端(客户端)的协议地址

addrled: 客户端地址长度

返回值

该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,accept函数接受一个客户端请求后会返回一个新的SOCKFD值,当有不同的客户端同时有不同请求时,会返回不同的SOCKFD的值。这个不同的值和建立SOCKET 时生成的SOCKFD还是不同的。服务器与客户端之间的通信就是在这些不同的SOCKFD上进行的。

而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。

6.数据收发 API:

字节流读取函数

套接字通信中进行字节读取函数: read() , write()。与 I/O中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少

ssize_t write(int fd, const void*buf,sizet nbytes);
ssize_t read(int fd,void *buf,size_t nbyte);

返回值: 读或写的字节个数,出错则返回-1

参数说明:

write 参数 :将buf中的nbytes个字节写入到文件描述符fd中,成功时返回写的字节数。
read 参数:为从fd中读取 nbyte个字节到buf 中,返回实际所读的字节数。

详细应用说明参考使用read write 读写socket(套节字)。
网络I/O还有一些函数
如: recv()/send(),readv()/writev(),recvmsg()/sendmsg(),recvfrom()/sendto()

7.数据收发常用第二套API

在TCP套接字上发送数据函数:有连接

ssize_t send(int s,const void *msg,size_t len,int flags);

//包含3要素:套接字s,待发数据msg,数据长度len
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0

在TCP套接字上接收数据函数:有连接

ssize_t recv(int s,void *buf,size_t len,int flags);

//包含3要素:套接字s,接收缓冲区buf,长度len
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0

8.客户端的connect函数

connect()函数:客户机连接主机

#include 
#include 

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

功能

该函数用于绑定之后的client 端(客户端),与服务器建立连接参数

参数说明:

sockfd:是目的服务器的 sockect 描述符

addr :是服务器端的IP 地址和端口号的地址结构指针

addrlen:地址长度常被设置为sizeof(struct sockaddr)

返回值

成功返回0,遇到错误时返回-1,并且errno 中包含相应的错误码

五、socket服务端代码实现:

服务器建立——可连接代码:

#include
#include
#include
//#include
#include
#include
#include
#include

int main()
{
    int s_fd;
    
    struct sockaddr_in s_addr;
    struct sockaddr_in c_addr;
    
    memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据 
    memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket

    s_fd = socket(AF_INET ,SOCK_STREAM,0);
    if(s_fd == -1){
           perror("socket");
           exit(-1);
     }

    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(8989);
    inet_aton("127.0.0.1", &s_addr.sin_addr);  //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API

//2.bind

    bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));

//3.listen

    listen(s_fd,10);   //不断监听外面是否有数据  ,listen配置完后,马上会结束
    int clen = sizeof(struct sockaddr_in);

//4.accept

    int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen);  
//通过套接字连上客户端,后续对连接选项都是用返回值 c_fd  来操作,s_fd 可能还需要接收其他人连接。
     if(c_fd == -1){
               perror("accept");
     }
     printf("get connect%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
//6.write
//7.close

printf("connect\n");
while(1);

return 0;
}

注意1 :

在 cd /usr/include/ 下收索结构体 struct sockaddr_in grep “struct

sockaddr_in {” *-nir
*-nir表示在当前目录下递归的 找,r是递归 n是显示行号,i是不区分大小写
在这里插入图片描述经过查找,在头文件加上#include

注意2:

s_addr.sin_port =(8888); 端口号用户要在5000以上 ,端口号要传到网络上去,所以我们要调用函数来变成网络字节序。

所以我们要知道字节序转换API

#include 
uint16_t  htons(uint16_t  host16bitvalue);   //返回网络字节序的值 
uint32_t  htonl(uint32_t   host32bitvalue);   //返回网络字节序的值
uint16_t  ntohs(uint16_t   net16bitvalue);    //返回主机字节序的值
uint32_t  ntohl( uint32_t   net32bitvalue);    //返回主机字节序的值

h代表hostn代表nets代表short(两个字节), l代表long(4个字节)
通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY ,INADDR_ANY 指定地址让操作系统自己获取

listen(s_fd,10); //不断监听外面是否有数据 ,listen配置完后,马上会结束
int c_fd = accept(s_fd,NULL,NULL); //判断是否有已经三次握手的,如果有,就继续往下,跟客户端进行连接,把客户端信息返回到c_fd;

注意3:

inet_aton(“127.0.0.1”, &s_addr.sin_addr);
//把字符串形式的“127.0.0.1”转为网络能识别的格式
把本机地址转变为网络可识别的地址,需要调用函数来转换,
int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);

结果显示:
虚拟机的服务端正在等待连接。
Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第3张图片
在windows下用cmd 命令来连接服务端
Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第4张图片
连接成功!
Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第5张图片
我们调用的 socket(AF_INET ,SOCK_STREAM,0);
AF_INET 是TCP协议,我们在windows端cmd下telnet也是TCP协议,所以可以连接上。

printf(“get connect%s\n”,inet_ntoa(c_addr.sin_addr));
char* inet_ntoa(struct in_addr inaddr);
把网络格式的IP地址转化为字符串形式

int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen); 返回客户端信息,打印客户端IP地址
在这里插入图片描述
服务器建立——可连接 —— 交互 ——代码:

#include
#include
#include
#include
#include
#include
#include

int main()
{
    int s_fd;
    int n_read;
    char readBuf[128];
    char *msg ="Refuel.CONG";

    struct sockaddr_in s_addr;
    struct sockaddr_in c_addr;

    memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据 
    memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket

    s_fd = socket(AF_INET ,SOCK_STREAM,0);
    if(s_fd == -1){
           perror("socket");
           exit(-1);
     }

    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(7777);
    inet_aton("127.0.0.1", &s_addr.sin_addr);  //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
    //2.bind

    bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));

//3.listen
    listen(s_fd,10);
    int clen = sizeof(struct sockaddr_in);
//4.accept

    int c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
    if(c_fd == -1){
           perror("accept");
    }
    printf("get connet%s\n",inet_ntoa(c_addr.sin_addr));


//5.read
    n_read = read(c_fd , readBuf ,128);
    if(n_read == -1){
          perror("read");
    }
    else{
          printf("get message:%d,%s\n",n_read,readBuf);
    }
//6.write
    write(c_fd,msg,strlen(msg));
//7.close
return 0;
}

在这里插入图片描述

六、socket客户端代码实现

#include
#include
#include
#include
#include
#include
#include

int main()
{
    int k_fd;
    int n_read;
    char readBuf[128];
    char *msg ="message from client \n";

    struct sockaddr_in k_addr;

    memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据 
    
//1.socket

    k_fd = socket(AF_INET ,SOCK_STREAM,0);
    if(k_fd == -1){
           perror("socket");
           exit(-1);
     }


    k_addr.sin_family = AF_INET;
    k_addr.sin_port = htons(7777);
    inet_aton("127.0.0.1", &k_addr.sin_addr);  //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
   
//3.connet
   if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
   {
      perror("connect");
      exit(-1);
   }
//4. send 
 
    write(k_fd,msg,strlen(msg));
//5. read
     
    n_read = read(k_fd , readBuf ,128);
    if(n_read == -1){
          perror("read");
    }
    else{
          printf("get message from sever:%d,%s\n",n_read,readBuf);
    }
//7.close
return 0;
}

打开服务端 ,服务端等待连接。。。打开客户端,连接成功,同时双端进行交互。

注意——连接的时候IP和端口号要一致:

    //服务端
    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(7777);
    inet_aton("127.0.0.1", &s_addr.sin_addr); 
    //客户端
    k_addr.sin_family = AF_INET;
    k_addr.sin_port = htons(7777);
    inet_aton("127.0.0.1", &k_addr.sin_addr);

在这里插入图片描述

七、实现双方聊天

在之前我讲进程那篇里,我们用输入的方式模拟了socket服务器对接客户端的应用场景

一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种情求达到时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

服务端代码:

#include
#include
#include
#include
#include
#include
#include

int main(int argc , char **argv)
{
    int s_fd;
    int c_fd;
    int n_read;
    char readBuf[128];
    char msg[] ={0};

    struct sockaddr_in s_addr;
    struct sockaddr_in c_addr;

    memset(&s_addr,0,sizeof(struct sockaddr_in));//清空数据 
    memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket

    s_fd = socket(AF_INET ,SOCK_STREAM,0);
    if(s_fd == -1){
           perror("socket");
           exit(-1);
     }

    s_addr.sin_family = AF_INET;
    s_addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &s_addr.sin_addr);  //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
   
//2.bind

    bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));

//3.listen
    listen(s_fd,10);
    int clen = sizeof(struct sockaddr_in);
//4.accept
    while(1)   //一直接收连接请求
    {
           c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
           if(c_fd == -1){
                   perror("accept");
            }
           printf("get connet :%s\n",inet_ntoa(c_addr.sin_addr));

      if(fork() == 0){ // 创建子进程  用来读取客户端
          if(fork() == 0){ // 在建子进程,用来与客户端发送
            while(1){
            
             memset(msg,0,sizeof(msg));
             printf("input(servel)  :");
             gets(msg);
             write(c_fd,msg ,strlen(msg));
           }   
       }   
        while(1){
              memset(readBuf,0,sizeof(readBuf));            
              n_read = read(c_fd , readBuf ,128);
              if(n_read == -1){
                    perror("read");
              }
               else{
                        printf("get message:  %d,%s\n",n_read,readBuf);
                   }
             }
             break;
      }
   }    
return 0;
}

客户端代码:

#include
#include
#include
#include
#include
#include
#include

int main(int argc ,char **argv)
{
    int k_fd;
    int n_read;
    char readBuf[128];
    char msg[] ={0};
 
    struct sockaddr_in k_addr;

    memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据 
    
//1.socket

    k_fd = socket(AF_INET ,SOCK_STREAM,0);
    if(k_fd == -1){
           perror("socket");
           exit(-1);
     }

    k_addr.sin_family = AF_INET;
    k_addr.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &k_addr.sin_addr);  //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
   
//3.connet
   if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
   {
      perror("connect");
      exit(-1);
   }
//4. send 
 while(1){
   if(fork() == 0){
      while(1){   
            memset(msg,0,sizeof(msg));
            printf("input(kehu)  :");
            gets(msg);
            write(k_fd,msg,strlen(msg));
      }
   }
//5. read
    
   while(1){
       memset(readBuf,0,sizeof(readBuf));      
       n_read = read(k_fd , readBuf ,128);
       if(n_read == -1){
            perror("read");
       }
       else{
           printf("get message from sever:%d,%s\n",n_read,readBuf);
       }
   }    
  }
//7.close
return 0;
}

服务端指令:
Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第6张图片

客户端指令:
Linux基础 ——“网络编程” 了解客户和服务器之间的故事_第7张图片
现在能实现双方通信但是有一个BUG就是 (资源竞争) ,无法实现多方通信。

你可能感兴趣的:(Linux,开发,linux,网络编程,socket套接字,服务器客户端,文件描述符)