第二段Linux编程之网络编程

一、网络编程的概述

网络:

  1. 地址(包括IP地址和端口号)
  2. 数据交流(涉及到了协议http、tcp、UDP说白了就是数据格式)
  3. 单片机会用到端口号协议(uart)

网络编程入门都讲socket套接字(套接字分两种tcp、UDP):

  • TCP :面向连接(想当于打电话,可信度高)
  • UDP :面向报文(相当于发短信,要是数据量大,内存响应还快,可靠度没那么高,就用它)

TCP/UDP对比:

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

端口号的作用:

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

二、字节序

x86系列、cpu都是Little endian的字节序;网络字节序=大端字节序

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

  1. Little endian(小端字节序):降低字节存储在起始地址。
  2. Big endian(大端字节序):将高字节存储在起始地址。

字节序单位:
(Byte)1个字节=8bit(点)
1个16进制要用4个2进制表示。

例子:在内存中双字(是32位)0x01020304(DWORD)的存储方式
内存地址:
4000&4001&4002&4003

LE 04 03 02 01
BE 01 02 03 04

字节序转换api:
头文件:#include<netinet/in.h>

  • 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代表host是主机的意思,n代表net是网络的意思,s代表short是短的意思(两个字节) 代表long (4个字节) 通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR ANY,INADDR_ANY指定地址让操作系统自己获取

三、socket编程步骤

socket服务器的开发步骤:

  1. scoket: 创建套接字,返回值是一个套接字描述符,是向后续的网络操作提供一个对接的接口
  2. bind: 为套接字添加信息(IP地址和端口号)把IP和端口号绑定到socket创建的通道上
  3. listen(): 监听网络连接
  4. accept(): 监听到有客户端接入,接受一个连接
  5. read、write等: 数据交互
  6. close :关闭套接字,断开连接

客户端的开发步骤:

  1. socket(): 给我一个通道,我知道你IP地址和端口号
  2. connect():调用这个函数就可以连接
  3. read、write等:数据交互

在这里插入图片描述


四、Linux提供的API简析

1、创建套接字 socket

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

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

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

type参数指定 socket的类型

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

protocol:

  • 通常赋值0

0 选择 type 类型对应的默认协议:

  • IPPROTO _UDP UDP 传输协议
  • IPPROTO _SCTP SCTP 传输协议
  • IPPROTO_TCP TCP 传输协议
  • IPPOTOR_TIPC TIPC传输协议

2、地址准备好

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

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

功能:

  • 用于绑定IP地址和端口号到socketfd

参数:

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

第二个参数用的结构体:

//IPV4对应的是
struct sockaddr {
	sa_family_t sa_family;//协议族
    char        sa_data[14];//IP+端口号
    };//这个一般不用,我们用下面这个,所以用时要做个强制指针转换
同等替换:
struct sockaddr_in {
	sa_family_t  sin_family;//协议族   
	int_port_t sin_port;//端口号    
  struct in_addr        sin_addr;//IP地址结构体     
  unsigned char        sin_zero[8];/*填充,没有实际意义,只是为了
  跟sockaddr结构体在内存中对齐,这样两者才能相互转换*/
  找到这个结构体的指令:
  cd /usr/include/   :/*进入到include目录*/
  grep "struct sockaddr_in {" * -nir  :/*当前目录下递归的查找引号中的
  东西,n:是显示行号;r:是递归;i:是不区别大小写*/
  vi linux/in.h +184   :/*进入并找到头文件的184行*/ 
};

我们定义了一个struct sockaddr_in的结构体后,要用memset函数把数据清空,如:memset(&s_addr,0,sizeof(struct sockaddr_in));

IP地址转换API:

int inet_aton(const char *straddr,struct in_addr *addrp);
//straddr :IP字符串      addrp :IP的内存地址
//把字符串形式的"192.168.1.123"转换为网络能识别的格式

char *inet_ntoa(atruct in_addr inaddr);
//把网络格式的IP地址转换为字符串形式

3、监听

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

int  listen(int  sockfd,int  backlog);

功能:

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

参数:

  • sockfd: 是 socket 系统调用返回的服务器端 socket 描述符
  • backlog: 指定在请求队列中允许的最大请求数

4、连接

accept()函数:

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

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

参数:

  • sockfd: 是socket 系统调用返回的服务器端 socket 描述符
  • addr: 用来返回已连接的对端(客户端)的协议地址,不关心设置NULL
  • addrled: 客户端地址长度,不关心设置NULL
    accept的第二个参数也是一个struct sockaddr_in的结构体。用法和bind的一样
    返回值:
    该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符,一个服务超通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示 TCP三次握手已完成)当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。

5、数据收发

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

 ssize_t read(int fd, void *buf, size_t count);
 ssize_t write(int fd, const void *buf, size_t count);

网络I/O中还有一些函数,例如:recv()/send(); readv()/writev()
recvmsg()/sendmsg(); recvfrom()/sendto(){后面两组一般用于UDP连接的}

数据收发常用的一套API:

(2)、在TCP套接字上发送数据函数:

ssize_t send(int s,const void *msg,size_t len,int flags);
  1. 包含3要素:套接字s,待发数据msg,数据长度len
  2. 函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述符,即accept函数的返回值
  3. 参数msg指向内存放待发送数据的缓冲区
  4. 参数len为待发数据的长度,参数flags为控制选项,一般设置为0

(3)、在TCP套接字上接收数据函数:

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

6、客户端的connect函数

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

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

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

参数:

  • sockfd: 是目前的服务器的socket描述符
  • addr: 是服务器端的IP地址和端口号的地址结构指针
  • addrlen:地址长度常被设置为sizeof(struct sockaddr)

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


五、socket服务器端代码实现

#include<stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <arpa/inet.h>
#include <netinet/in.h>
#include<stdlib.h>
#include<string.h>

// int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);


int main(int argc,char **argv)
{
        int s_fd;
        int c_fd;
        char readbuf[128];
        int n_read;
        //      char *msg = "I get your message";
        char msg[128] = {0};
        int mark = 0;

        struct sockaddr_in s_addr;/*在bind里要用的结构体,
        它在内核里定义好啦,声明下就可以使*/
        struct sockaddr_in c_addr;

        if(argc != 3){
                printf("param is not good\n");
                exit(-1);
        }

        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]));/*这里涉及到了端口号,
  我们一般用5000~9000的5000以下是操作系统的,这里还涉及到了字节序的问题
  ,因为x86是小端而网络是大端,这里用到了字节序转换api*/
        inet_aton(argv[1],&s_addr.sin_addr);
        //2. bind
        bind(s_fd,(struct sockaddr *)&s_addr,
        sizeof(struct sockaddr_in));
        //3.listen
        listen(s_fd,10);//这里有两个队列,两个队列加起来是10
        //4.accept
        int clen = sizeof(struct sockaddr_in);

        while(1){
                c_fd = accept(s_fd,(struct sockaddr*)&c_addr,&clen);/*
 这个函数会检测到要连接的客户端,连接成功返回一个连接成功的套接字描述符
 ,后续操作靠它*/
      
                if(c_fd == -1){
                        perror("accept");
                }
                mark++;
                printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));

                if(fork() == 0){


                        if(fork() == 0){
                                while(1){
                     sprintf(msg,"get:welcom No.%d client",mark);
                                        write(c_fd,msg,strlen(msg));
                                        sleep(3);
                                }
                        }
                        while(1){
                                //5.read
                                memset(readbuf,0,sizeof(readbuf));
                                n_read = read(c_fd,readbuf,128);
                                if(n_read == -1){
                                        perror("read");
                                }else{
                       printf("getmessage:%d,%s\n",n_read,readbuf);
                                }
                        }
                        break;
                }
        }



        return 0;
}
                                                                                                   
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值