简单实现一个linux下的socket server 和 client

目录

1、linux文件描述符

2、在Linux下创建 socket

3、bind()函数和connect()函数

3.1、bind()函数

3.2、connect()函数

4、listen()函数和accept()函数

4.1、listen()函数

4.2、accept()函数

5、write()和read()

5.1、write()函数

5.2、read()函数

6、send()和recev()

7、一个service和client的简单实现


1、linux文件描述符

在linux中,一切都是文件。一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件。例如,stdin 称为标准输入文件,它对应的硬件设备一般是键盘,stdout 称为标准输出文件,它对应的硬件设备一般是显示器。对于所有的文件,都可以使用 read() 函数读取数据,使用 write() 函数写入数据。

“一切都是文件”的思想极大地简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样。所有在Linux中创建的文件都有一个 int 类型的编号,称为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可以。例如,stdin 的描述符为 0,stdout 的描述符为 1。

在Linux中,socket 也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件 I/O 相关的函数。可以认为,两台计算机之间的通信,实际上是两个 socket 文件的相互读写。

文件描述符有时也被称为文件句柄(File Handle),但“句柄”主要是 Windows 中术语。

2、在Linux下创建 socket

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);
  • af:地址族(Address Family),也就是IP地址类型,常用的有AF_INET和AF_INET6。AF是"Address Family"的简写,INET是"Internet"的简写。AF_INET表示IPv4地址。例如127.0.0.1;AF_INET6表示IPv6地址,例如1030::C9B4:FF12:48AA:1A2B。你也可以使用PF前缀,PF是"Protocol Family"的简写,它和AF是一样的。例如,PF_INET等价于AF_INET,PF_INET6等价于AF_INRT6。
  • type:数据传输方式,常用的有SOCK_STREAM和SOCK_DGRAM
  • protocol表示传输协议,常用的有IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP传输协议和UDP协议。

看到这儿,你可以能有个疑问,有了IP地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

没错,一般情况下有了af和type两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同额协议支持同一种IP地址类型和数据传输方式。如果我们不知名使用哪种协议,操作系统是没办法自动推演的、

如果af的值设置为PF_INET,使用SOCK_STREAM传输方式,那么满足这两个条件的协议只有TCP,因此可以这样来调用SOCK()函数:

int tcp_socket = socket(AD_INET,SOCK_STREAM,IPPROTO_TCP);  //TCP套接字

如果af的值设置为PF_INET,使用SOCK_DGRAM传输方式,那么满足这两个条件的协议只有UDP,因此可以这样来调用SOCKET()函数:

int udp_socket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP); //UDP套接字

上面的两种情况都只有一种协议满足条件,可以将protocol的值设为0,系统会自动推演出应该使用什么协议,代码如下所示

int tcp_socket = socket(AD_INET,SOCK_STREAM,0);  //TCP套接字

int udp_socket = socket(AF_INET,SOCK_DGRAM,0); //UDP套接字

3、bind()函数和connect()函数

socket()函数用来创建套接字,确定套接字的各种属性,然后服务器端要用bind()函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字;而客户端要用connect()函数建立连接。

3.1、bind()函数

bind()函数的原型为:

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

sock为socket文件描述符,addr为sockaddr结构体变量指针,addrlen为addr变量的大小。

socklen_t的定义 ,其实就是一个uint32。

我们来看一个代码,将创建的套接字与IP地址128.0.0.1、端口1123绑定:

int serv_sock = sock(AF_INET,SOCK_STREAM,IPPROTO);  //创建一个TCP套接字

struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(servaddr));
serv_addr.sin_famil = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1123);  //端口

bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr));

我们来看一下sockaddr_in结构体

struct sockaddr_in
{
    sa_family_t    sin_family;  //地址族(Address Family),也就是地址类型
    unit16_t       sin_port;    //16位的端口号
    struct in_addr sin_addr;   //32位IP地址
    char           sin_zero[8]; //不使用,一般用0填充
}
  1. sin_family和socket()的第一个参数的含义相同,取值也要保持一致。
  2. sin_port为端口号,uint16_t的长度为两个字节,理论上端口号的取值范围为0~65536,但0~1023的端口一般由系统分配给特定的服务程序,例如web服务的端口为70,FTP服务的端口为21,所以我们的程序尽量在1024~65536之间分配端口号。
  3. sin_addr是struct in_addr结构体类型的变量。
  4. sin_zero是多于的8个字节,没有用,一般使用memset()函数填充为0。上面的代码中,先用memset()将结构体的全部字节填充为0,再给前三个成员赋值,剩下的sin_zero自然就是0了。

in_addr结构体

sockaddr_in的第三个成员是in_addr类型的结构体,该结构只包含一个成员,如下所示:

struct in_addr
{
    in_addr_t s_addr;  //32位的IP地址
};

in_addr_t在头文件<netinet/in.h>中定义,等价于unsigned long,长度为四个字节。也就是说,s_addr是一个整数,而IP地址是一个字符串,所以需要inet_addr()函数进行转换,例如:

unsigned long ip = inet_addr("127.0.0.1");

运行结果:

为什么使用sockaddr_in而不使用sockaddr?

bind()第二个参数的类型为sockaddr,而代码中却使用sockaddr_in,然后再强制转换为sockaddr。

sockaddr结构体的定义如下:

struct sockaddr
{
    sa_family_t sin_family;   //地址族
    char        sa_data[14];  //IP地址和端口号
}

下图是sockaddr与sockaddr_in的对比(括号中的数字表示所占用的字节数)

sockaddr和sockaddr_in的长度相同,都是16个字节,但是sockaddr的sa_data区域需要同时指定IP地址和端口号,例如"127.0.0.1:8080",遗憾的是没有相关函数将这个字符串转换成需要的形式,也就很难给sockaddr类型的变量直接赋值,所以使用sockaddr_in来代替。这两个结构体的长度相同,强制转换类型时也不会丢失字节,也没有多于的字节。

可以认为,sockaddr是一个通用的结构体,可以用来保护多种类型的IP地址和端口号,而sockaddr_in是专门用来存IPv4的结构体。另外还有sockaddr_in6,用来保存IPv6地址,它的定义如下:

struct sockaddr_in6
{
    sa_family_t sin6_family;   //IP地址类型,取值为AF_INET6
    in_port_t   sin6_port;     //16位端口号
    uint32_t sin6_flowinfo;    //IPv6流信息
    struct in6_addr sin6_addr; //具体的IPv6地址
    unit32_t sin6_scpoe_id;    //接口范围ID
};

in.h中声明的sockaddr_in和sockaddr_in6如下: 

3.2、connect()函数

connect()函数用来建立连接,它的原型为:

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

各个参数的说明与bind()函数相同。

4、listen()函数和accept()函数

对于服务器端程序,使用bind()绑定套接字后,还需要使用listen()函数让套接字进入被动监听状态,再调用accept()函数,就可以随时响应客户端的请求了。

4.1、listen()函数

通过listen()函数可以让套接字进入被动监听状态,它的原型为:

int listen(int sock,int backlog)

sock为需要进入监听状态的套接字,backlog为请求队列的最大长度

所谓被动监听,是指在没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。

请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它先放到缓冲区中,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列。

缓冲区的长度(能存放多少个客户端的请求)可以通过listen()函数的backlog参数指定,但究竟为多少并没有什么标准,根据你的需求来定。

如果将backlog的值设置为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百或者更多。

当请求队列满时,就不再接收新的请求,对于linux,客户端会受到ECONNREFUSED错误。

注意:listen()函数只是让套接字处于监听状态,并没有接收请求。接收请求需要使用accept()函数来阻塞进程执行,直到有新的请求到来。

4.2、accept()函数

当套接字处于监听状态时,可以通过acceot()函数来接收客户端请求。它的原型为:

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

它的参数与listen()和connect()是相同的;sock为服务器端套接字,addr为sockaddr_in结构体变量,addrlen为参数addr的长度,可以由sizeof()求得。

accept()返回一个新的套接字和客户端通信,addr保存了客户端的IP地址和端口号,而sock是服务器端的套接字。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。

5、write()和read()

linux不区分套接字文件和普通文件,使用write()可以向套接字中写入数据,使用read()可以从套接字中读取数据。

两台计算机之间得通信相当于两个套接字之间得通信,在服务端用write()向套接字写入数据,客户端就能收到,然后再使用read()从套接字中读取出来,就完成了一次通信。

5.1、write()函数

write()的原型为:

/*
fd:待写入的文件的描述符
buf:待写入的数据的缓冲区地址
nbytes:写入的数据的字节数
ssize_t:signed int
*/
ssize_t write(int  fd,const void *buf,size_t nbytes);

write()函数会将缓冲区buf中的nbytes个字节写入文件fd,成功则返回写入的字节数,失败返回-1。

5.2、read()函数

/*
fd:待读取的文件的描述符
buf:待读取的数据的缓冲区地址
nbytes:读取的数据的字节数
ssize_t:signed int
*/
ssize_t read(int  fd,void *buf,size_t nbytes);

read()函数会从fd文件中读取nbytes个字节并保存在缓冲区buf中,成功则返回读取到的字节数(遇到文件结尾返回0),失败则返回-1。

6、send()和recev()

/* Send N bytes of BUF to socket FD.  Returns the number sent or -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);

/* Read N bytes into BUF from socket FD.
   Returns the number read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);

/* Send N bytes of BUF on socket FD to peer at address ADDR (which is
   ADDR_LEN bytes long).  Returns the number sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
                       int __flags, __CONST_SOCKADDR_ARG __addr,
                       socklen_t __addr_len);

/* Read N bytes into BUF through socket FD.
   If ADDR is not NULL, fill in *ADDR_LEN bytes of it with tha address of
   the sender, and store the actual size of the address in *ADDR_LEN.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
                         int __flags, __SOCKADDR_ARG __addr,
                         socklen_t *__restrict __addr_len);


/* Send a message described MESSAGE on socket FD.
   Returns the number of bytes sent, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t sendmsg (int __fd, const struct msghdr *__message,
                        int __flags);

#ifdef __USE_GNU
/* Send a VLEN messages as described by VMESSAGES to socket FD.
   Returns the number of datagrams successfully written or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sendmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags);
#endif

/* Receive a message as described by MESSAGE from socket FD.
   Returns the number of bytes read or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern ssize_t recvmsg (int __fd, struct msghdr *__message, int __flags);

#ifdef __USE_GNU
/* Receive up to VLEN messages as described by VMESSAGES from socket FD.
   Returns the number of messages received or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int recvmmsg (int __fd, struct mmsghdr *__vmessages,
                     unsigned int __vlen, int __flags,
                     struct timespec *__tmo);
#endif

7、一个service和client的简单实现

/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:service.c
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

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

int main(){
    //创建套接字
    int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    //将套接字和IP、端口绑定
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //进入监听状态,等待用户发起请求
    listen(serv_sock, 20);

    //接收客户端请求
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    while(1)
    {
        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

        //向客户端发送数据
        char str[] = "不要艾特我";
        write(clnt_sock, str, sizeof(str));

        //关闭套接字
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}
/*================================================================
 *   Copyright (C) 2021 baichao All rights reserved.
 *
 *   文件名称:client.cpp
 *   创 建 者:baichao
 *   创建日期:2021年01月22日
 *   描    述:
 *
 ================================================================*/

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

int main(){
    //创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);

    //向服务器(特定的IP和端口)发起请求
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
    serv_addr.sin_family = AF_INET;  //使用IPv4地址
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
    serv_addr.sin_port = htons(1234);  //端口
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

    //读取服务器传回的数据
    char buffer[40];
    read(sock, buffer, sizeof(buffer)-1);

    printf("Message form server: %s\n", buffer);

    //关闭套接字
    close(sock);

    return 0;
}

运行结果:

启动server

server处于监听状态

启动客户端:

至此,一个简易的socket通信代码完成

猜你喜欢

转载自blog.csdn.net/weixin_40179091/article/details/113024907