Linux下Socket编程

发布于:2022-12-11 ⋅ 阅读:(238) ⋅ 点赞:(0)

文章目录

tcp服务器客户端编程流程

主机字节序列和网络字节序列

​编辑

Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:

套接字地址结构

通用socket地址结构 

专用 socket 地址结构

IP 地址转换函数

网络编程接口

服务端的工作流程

客户端的工作流程


学习socket编程前要先明白网络应用程序通信流程

应用程序 A 要将数据”hello” 传给网络上另外一台主机上的应用程序 B, 数据“hello”从应用层发送给传输层后,传输层在数据前面加上 tcp 协议或 udp 协议的报头,将整条报文发给网络层,网络层添加自己的 IP 报头,再将整条数据发送给数据链路层。数据链路层将数据封装成能在网络中独立传输的数据单元,即数据帧。封装好的数据帧通过网络传输到另一台主机,然后再从下层依次拆包,将数据部分送往应用层。应用程序 B 就得到了数据” hello” 。

aaeeb94fdc914a4ea52b801ce2ead2fb.png

要想客户端和服务器能在网络中通信,那必须得使用 Socket 编程,它是进程间通信里比较特别的方式,特别之处在于它是可以跨主机间通信。

Socket 的中文名叫作插口,咋一看还挺迷惑的。事实上,双方要进行网络通信前,各自得创建一个 Socket,这相当于客户端和服务器都开了一个“口子”,双方读取和发送数据的时候,都通过这个“口子”。这样一看,是不是觉得很像弄了一根网线,一头插在客户端,一头插在服务端,然后进行通信。

创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP。

UDP 的 Socket 编程相对简单些,这里我们只介绍基于 TCP 的 Socket 编程。

服务器的程序要先跑起来,然后等待客户端的连接和数据,我们先来看看服务端的 Socket 编程过程是怎样的。

tcp服务器客户端编程流程

48c7e4f8c27b49e7aab836b132ba9116.png

socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。

这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用

的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM) 。

bind()方法是用来指定套接字使用的 IP 地址和端口。 IP 地址就是自己主机的地址,

如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16

位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般

用户不能随便使用。其次, 1024-4096 为保留端口, 用户一般也不使用。 4096 以

上为临时端口,用户可以使用。在Linux 上, 1024 以内的端口号,只有 root 用户

可以使用。

listen()方法是用来创建监听队列。 监听队列有两种,一个是存放未完成三次握手的

连接,一种是存放已完成三次握手的连接。 listen()第二个参数就是指定已完成三次

握手队列的长度。

accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连

接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻

塞。

connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。

该方法执行后,会进行三次握手, 建立连接。

9fb2c9a1fe784a0d8b232e737c880997.png

send()方法用来向 TCP 连接的对端发送数据。 send()执行成功,只能说明将数据成

功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。 send()的返

回值为实际写入到发送缓冲区中的数据长度。

recv()方法用来接收 TCP 连接的对端发送来的数据。 recv()从本端的接收缓冲区中读

取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字

节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。

close()方法用来关闭 TCP 连接。此时,会进行四次挥手。

099a6531853f4f7b9891f874ed72f993.png

主机字节序列和网络字节序列

主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。

大端:手机,网络

套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。

小端:电脑

我们使用的 PC 机,数据的存储默认使用的是小端

2315b9b94cb04bdbb0b1f3e1011411b6.png

a7fc138583c74eca993a9b56d32018a7.png

Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:

主机字节序列:大端/小端

网络字节序列:大端

#include <netinet/in.h> 

本地->网络ip
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序 
网络->本地ip
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序 
本地-网络port
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序 
网络->本地port
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序

套接字地址结构

通用socket地址结构

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h> 

struct sockaddr 

{

	sa_family_t sa_family;//协议族 

	char sa_data[14];//数据,没有给出IP地址,就是给了这么一块儿空间,起了一个占位的作用. 

};

sa_family 成员是地址族类型(sa_family_t) 的变量。地址族类型通常与协议族类型

对应。常见的协议族和对应的地址族如下图所示:

2db42f03a9cc4903a246553cce4e735b.png

专用 socket 地址结构

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们

分别用于 IPV4 和 IPV6:

//sin_family: 地址族 AF_INET 

//sin_port: 端口号,需要用网络字节序表示 

//sin_addr: IPV4 地址结构: s_addr 以网络字节序表示 IPV4 地址 

struct in_addr 

{
	u_int32_t  s_addr;//无符号的32位的整型,存放IP地址; 
};

//tcp协议族 

struct sockaddr_in 

{

	sa_family_t sin_family;//地址族,就是sin_family: 地址族 AF_INET 

	u_int16_t sin_port;//端口,16位的端口 

	struct in_addr sin_addr;//一个结构体,只有一个成员,是无符号的32位的整型, 

	存放IP地址;(IPV4的地址就是32位) 

	//其实后面还有占位的,只是我们不用它,所以就没有写; 

};

//tcp协议族就主要有三个:地址族,端口号,IP地址 

//IP协议族 

struct in6_addr 

{

unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示 

};

struct sockaddr_in6 
{ 

	sa_family_t sin6_family; // 地址族: AF_INET6 

	u_inet16_t sin6_port; // 端口号:用网络字节序表示 

	u_int32_t sin6_flowinfo; // 流信息,应设置为 0 

	struct in6_addr sin6_addr; // IPV6 地址结构体 

	u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段 

};

IP 地址转换函数

虽然 IP 地址本质是一个整形数 ,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的 IP 地址进行大小端转换:

// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 

参数:

af: 地址族 (IP 地址的家族包括 ipv4 和 ipv6) 协议

AF_INET: ipv4 格式的 ip 地址

AF_INET6: ipv6 格式的 ip 地址

src: 传入参数,对应要转换的点分十进制的 ip 地址: 192.168.1.100

dst: 传出参数,函数调用完成,转换得到的大端整形 IP 被写入到这块内存中

返回值:成功返回 1,失败返回 0 或者 - 1


#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址        
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

af: 地址族协议

AF_INET: ipv4 格式的 ip 地址

AF_INET6: ipv6 格式的 ip 地址

src: 传入参数,这个指针指向的内存中存储了大端的整形 IP 地址

dst: 传出参数,存储转换得到的小端的点分十进制的 IP 地址

size: 修饰 dst 参数的,标记 dst 指向的内存中最多可以存储多少个字节

返回值:

成功:指针指向第三个参数对应的内存地址,通过返回值也可以直接取出转换得到的 IP 字符串 失败: NULL

注意:

下面两个函数只能用于ipv4

通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序

整数表示的 IPV4 地址之间的转换:

#include <arpa/inet.h> 

in_addr_t inet_addr(const char *cp); //字符串表示的 IPV4 地址转化为网络字节序 

char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示

网络编程接口

#include <sys/types.h> 

#include <sys/socket.h> 

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

//socket()创建套接字,成功返回套接字的文件描述符,失败返回-1 

//domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6 

//type: 设置套接字的服务类型 流服务SOCK_STREAM(tcp)  数据报服务SOCK_DGRAM(udp) 

// protocol: 一般设置为 0,表示使用默认协议 

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

//bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1 

//sockfd 是网络套接字描述符,(命名套接字,就是上面的函数的返回值作为了我们的参数 sockfd) 

//addr 是地址结构 

//addrlen 是 socket 地址的长度

int listen(int sockfd, int backlog); 

//listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1 

//sockfd 是被监听的 socket 套接字 

//backlog 表示处于完全连接状态的 socket 的上限 

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

//accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket, 

//该 socket 唯一地标识了被接收的这个连接,失败返回-1 

//sockfd 是执行过 listen 系统调用的监听 socket 

//addr 参数用来获取被接受连接的远端 socket 地址 

//addrlen 指定该 socket 地址的长度 

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

//connect()客户端需要通过此系统调用来主动与服务器建立连接, 

//成功返回 0,失败返回-1 

//sockfd 参数是由 socket()返回的一个 socket。 

//serv_addr 是服务器监听的 socket 地址 

//addrlen 则指定这个地址的长度 

int close(int sockfd); 

//close()关闭一个连接,实际上就是关闭该连接对应的 socket 

ssize_t recv(int sockfd, void *buff, size_t len, int flags); 


ssize_t send(int sockfd, const void *buff, size_t len, int flags); 

//TCP 数据读写: 

//recv()读取 sockfd 上的数据, buff 和 len 参数分别指定读缓冲区的位置和大小 

//send()往 socket 上写入数据, buff 和 len 参数分别指定写缓冲区的位置和数据长度

//flags 参数为数据收发提供了额外的控制 

UDP 数据读写: 

ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen); 

ssize_t sendto(int sockfd, void *buff, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);

//recvfrom()读取 sockfd 上的数据, buff 和 len 参数分别指定读缓冲区的位置和大小 

//src_addr 记录发送端的 socket 地址 

//addrlen 指定该地址的长度 

//sendto()往 socket 上写入数据, buff 和 len 参数分别指定写缓冲区的位置和数据长度 

//dest_addr 指定接收数据端的 socket 地址 

//addrlen 指定该地址的长度

了解上述API,我们就可以根据通信流程图写出服务器与客户端通信的代码了

服务端的工作流程

1)创建服务端的socket。

2)把服务端用于通信的地址和端口绑定到socket上。

3)把socket设置为监听模式。

4)接受客户端的连接。

5)与客户端通信,接收客户端发过来的报文后,回复处理结果。

6)不断的重复第5)步,直到客户端断开连接。

7)关闭socket,释放资源。

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

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字 使用ipv4地址
    assert(sockfd!=-1);
    
    struct sockaddr_in saddr,caddr; //saddr服务端专用的socket地址结构,caddr为客户端
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;//专用的ipv4强转为通用的
    saddr.sin_port=htons(6000);// 大端端口
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//测试用了回环地址
    int res=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));// 将socket()返回值和本地的IP端口绑定到一起
					//(struct sockaddr *)&saddr)把ipv4的专用接口转成通用接口
    assert(res!=-1);

    //设置监听队列为5
    listen(sockfd,5);
    //接收客服端连接
   		while(1)
        {
        int len=sizeof(caddr);//地址长度
        printf("accept wait....\n");
        //放在while里面,服务器一直开着
        int c=accept(sockfd,(struct sockaddr *)&caddr,&len);
        //c是链接套接字
        if(c<0)
        {
           continue;
        }
    
        printf("accept c=%d\n客户端ip:(%s)已连接;客户端端口port:(%d)已连接\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); 
     
        //客户端通信,接收客户端发过来的报文后,回复信息。
        while(1)
        {
           char buff[128]={0};
         
       	   int n=recv(c,buff,127,0);//接收客户端的请求报文
           if(n<=0) 
           {
               break;
           }
       	 printf("recv(%d):buff=%s\n",n,buff);//recv(%d)返回值>0就是接收的字节数
    	 char *str="ok,server had receved";
       	 send(c,str,strlen(str),0);//向客户端发送响应的结果 
        }
       close(c);//关闭链接套接字      
    }
    close(sockfd);
  
    exit(0);
}

客户端的工作流程

1)创建客户端的socket。

2)向服务器发起连接请求。

3)与服务端通信,发送一个报文后等待回复,然后再发下一个报文。

4)不断的重复第3)步,直到全部的数据被发送完。

5)第4步:关闭socket,释放资源。

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

int main()
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    assert(sockfd!=-1);
	//创建客户端socket
    struct sockaddr_in saddr;
    memset(&saddr,0,sizeof(saddr));
    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);

    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//回环地址
	//向服务器发起连接请求。
    
    int res=connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
    assert(res!=-1);
	//与服务端通信,发送一个报文后等待回复,循环发送数据,如果输入over则结束发送请求
    while(1)
    {
    	printf("input:\n");
   	    char buff[128]={0};
   		fgets(buff,127,stdin);
        if(strncmp(buff,"over",4)==0)
        {
            break;
        }
    	send(sockfd,buff,strlen(buff),0);//发送请求
         memset(sockfd,buff,sizeof(buff));
   	    recv(sockfd,buff,127,0);//接收来自服务器的回应
        printf("read:%s\n",buff);
   }
    close(sockfd);
    exit(0);
}

ac8a1a40888a49afa1fe6138839ff748.png

演示之后我们其实会发现,这样编写的代码其实只能实现一个服务器和客户端之间的连接,如果我们在开一个客户端与其连接,发送数据,上一个客户端如果不继续发送数据或者不断开连接,那么服务器就会一直阻塞在那里,服务器accept之后会一直阻塞在recv那里,对于设计者而言不可能大费周章的只能实现一对一的连接,如何改进让服务器能与所有的客户端连接,就是我下期就写到的内容了~

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到