Scoket编程快速入门(全面干货)

Socket编程快速入门(全面干货)

        这篇文章是我查询相关资料,学习相关视频,自己编写代码验证,进行总结的精华干货,我建议的学习方式是查看相关概念和函数后,自己按照流程编写服务器与客户端程序,遇到不会的查看示例代码,如有错误,欢迎评论区指正,有什么问题也可以在评论区指出,欢迎大家讨论,我看到评论会回复,感谢关注支持,希望大家一起进步,谢谢大家。

目录

Socket编程快速入门(全面干货)

一、理解Socket的基础概念

二、socket编程使用的相关函数

Socket地址结构

 常见Socket的数据类型

字节序

ip地址转换:

三、Socket编程核心函数

1.create()创建一个遵循某种传输协议的套接字,套接字会自动绑定在创建其的服务进程上:

2.bind()绑定服务器进程的监听套接字socket文件描述符和本地服务器的IP地址与固定端口号

3.listen()本地服务的监听套接字设置监听客户端的连接:

4.accept()阻塞并等待接受客户端的连接请求

5.connect()客户端自动绑定并连接指定服务器服务进程的IP地址和端口号:

6.read()/recv()通过套接字socket文件描述符读入/接收网络通信的数据:

7.write()/send()通过套接字socket文件描述符写出/发送网络通信的数据:

8.关闭套接字socket文件描述符的网络连接:

四、Socket编程的TCP通信流程与UDP通信流程

TCP通信流程

UDP通信流程:

  五、Socket编程的套接字通信工作原理

  六、TCP服务器要注意的2个问题:

  七、简单的服务器与客户端示例代码(详细代码注释)

1.综合进程等Linux系统编程的服务器与客户端TCP示例代码

2.简单的socket编程TCP服务器和客户端示例代码

一、理解Socket的基础概念

        Socket(套接字)是网络通信的端点,本质上是操作系统提供的一种抽象接口,为应用程序提供了使用网络协议进行通信的能力。它位于应用层和传输层之间,充当着应用程序与网络协议栈之间的桥梁,网络上的不同主机通过IP地址进行寻址以方便互相找到对方主机,在此基础上主机会运行很多服务程序,这些服务程序如果需要访问网络就需要一个可以访问外部的接口,这个接口就是“端口”,Socket(套接字)的寻址就是通过这两部分构成,网络上一台主机上的一个服务程序的数据就通过Socket(套接字)及相关协议和各种层,将数据发送到网络上对应IP地址的另一台主机的对应端口号绑定的服务程序上。

1.局域网:局域网将一定区域内的各种计算机、外部设备和数据库连接起来形成计算机通信的私有网络,实际上网的每台主机都在某个局域网下,局域网内主机可直接通信。
        广域网:又称广域网、外网、公网。是连接不同地区局域网或城域网计算机通信的远程公共网络,不同局域网的主机之间要进行通信要通过广域网。

2.ip地址:在局域网或广域网上的主机或服务器上的某个联网接口(物理/虚拟网卡)的上网地址,被用于表示计算机在网络中的地址,一般在局域网上的主机都使用私有ip地址,
        私有ip地址只能在本局域网内使用,广域网上使用公有ip地址通信,但局域网内私有ip地址的主机可通过具有公有ip地址的路由器转换,通过广域网与外网主机或服务器进行通信。
        IPV4的ip地址:4字节32位unisigned int无符号整形数,但IPV4的IP地址使用点分十进制表示的字符串,IPV6的IP地址:16字节128位数
        私有ip地址:10.0.0.0 - 10.255.255.255  172.16.0.0 - 172.31.255.255  192.168.0.0 - 192.168.255.255
        本地回环地址:127.0.0.0,一般用于允许在不离开本机的情况下进行网络操作,如访问运行在本机上的服务:Web服务器、数据库服务器等,而不需要通过网络接口。

3.域名:由于直接记忆IP地址过于复杂,使用点分开的具有意义的英文字母来代替IP地址,表示网络上某个主机或服务器,访问某个服务器时可使用域名代替IP地址,DHCP协议会自动将域名转换成IP地址,编程时也可直接使用域名转化为IP地址的函数实现。

4.端口:2字节16位unsigned short无符号短整形数,0 - 65535,网络通信中用于标识本机特定服务或进程的标识符。不参与网络通信的进程和线程不需要端口号,而参与网络通信的进程会绑定到一个端口号以便在网络上进行进程间端到端的数据交换,端口是绑定主机进程,一个进程内有多个线程同时进行网络操作,它们也会通过同一个进程绑定的端口号进行通信,服务器进程\线程使用特定的固定端口号为客户端提供服务,客户端进程\线程使用动态的端口号连接服务器,并且用完就丢弃,包括以下3类及适应范围。
        知名端口号:0 - 1024:80(HTTP),443(HTTPS),53(DNS),23(Telnet),20/21(FTP),25(SMTP)等,用于系统某些特地服务,用户和应用进程不可使用
        注册端口号:1024 - 49151,用于用户或应用程序,通常被用来运行特定的用户服务或应用程序。
        动态端口号:49152-65535,用于操作系统为用户进程/线程随机分配一个未使用的临时的端口号进行请求服务,临时使用完就丢弃。

5.网络分层模型及标准协议

(1)网络接口层(物理层,数据链路层:ARP协议);

(2)网络互联层(网络层:IP协议);

(3)传输层(传输层:TCP协议,UDP协议);

(4)应用层(会话层,表示层,应用层:HTTP协议,FTP协议,DHCP协议,SSH协议)
   TCP协议:一个面向连接的,安全的,流式传输的端到端的传输层协议。

  • 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。

         三次握手的连接过程:客户端向服务器发出连接请求=>服务器同意客户端的连接请求(建立客户端->服务器的单向连接),并向客户端发出连接请求 =>客户端同意服务器的连接请求(建立服务器->客户端的单向连接),并可向服务器发出数据
          四次挥手的断连过程:客户端向服务器发出断连请求=>服务器同意客户端的断连请求(断开客户端->服务器的单向连接)服务器向客户端发出断连请求=>客户端同意服务器的断连请求(断开服务器->客户端的单向连接)

  • 安全:tcp通信过程中,会对发送的每一数据包都会进行校验确认的回复, 如果发现数据丢失,则会自动重传。
  • 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致。

    UDP协议:一个面向无连接的,不安全的,报文传输的端到端的传输层协议。

        服务器与客户端交换数据进行通信不需要先建立连接,服务器与客户端以数据报文的方式随时传输数据,因此没有检测重传机制,数据报文可能会丢失不安全。

二、socket编程使用的相关函数

        一般在Linux环境下进行Socket编程,Windows环境下和Linuxs环境下的socket编程使用到的结构体变量类型和api函数几乎一致,未说明为Linuxs环境,显示说明为Windows环境

  • Socket地址结构

        包括struct sockaddrstruct sockaddr_in(IPV4)、struct sockaddr_in6(IPV6)、struct sockaddr_un(同主机进程通信)

        通常采取struct sockaddr_in类型参数addr_in赋值端口号、IP地址和地址族协议枚举值AF_INET (IPV4),强制转化成struct sockaddr类型addr后给函数使用。

// 举例构建struct sockaddr_in的socket地址,然后转化为struct sockaddr类型
struct in_addr ipaddr={inet_addr("101.152.155.102")}; 
struct sockaddr_in addr_in={AF_INET,10001,ipaddr}; 
struct sockaddr* addr=(struct sockaddr*) &addr_in; // 指针强制类型转换

//struct sockaddr结构体不方便赋值端口号和IP地址,但socket函数需要该类型的参数
struct sockaddr {
        sa_family_t sa_family;// 地址族协议:AF_INET=ipv4,AF_INET6=ipv6
        char        sa_data[14];// 网络字节序:端口(2字节) + IP地址(4字节) + 填充(8字节)
};

//struct sockaddr_in结构体方便赋值端口号和IP地址,函数不使用该类型的参数
struct sockaddr_in{
        sa_family_t sin_family;//地址族协议: 只能使用IPV4协议AF_INET
        in_port_t sin_port;//端口号, 网络字节序的2字节16位无符号短整形数
        struct in_addr sin_addr;//IP地址, struct in_addr结构体变量sin_addr存放网络字节序的4字节32位无符号整形数IP地址
        unsigned char sin_zero[8];//填充的8字节,自动填充对齐
};

// struct sockaddr_in的sin_addr变量类型
struct in_addr{
       in_addr_t s_addr;//网络字节序的4字节32位无符号整形的IP地址
};


//(Windows)和Linuxs的结构体struct sockaddr_in完全类似,唯一不同在与struct in_addr结构体
struct sockaddr_in {
     sin_family;//Address family
     unsigned short int sin_port;//Port number
     struct in_addr sin_addr;//Internet address
     unsigned char sin_zero[8];//Same size as struct sockaddr
};
typedef struct in_addr {(Windows)
     union {
  	   struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
  	   struct{ unsigned short s_w1, s_w2;} S_un_w;
           //初始化直接给S_un.S_addr赋值网络字节序的32位无符号长整形的IP地址即可
  	   unsigned long S_addr;} S_un;
 }IN_ADDR;
  •  常见Socket的数据类型

unsigned short(16位2字节无符号短整形)==uint16_t==in_port_t(端口号数据类型)
unsigned int(32位4字节无符号整形)==uint32_t==in_addr_t(IPV4地址数据类型)
unsigned short(16位2字节无符号短整形)==unsigned short int(16位2字节无符号短整形)==sa_family_t(IPV4和IPV6协议的枚举值数据类型:AF_INET=ipv4,AF_INET6=ipv6)
  • 字节序

        大于一个字节类型的数据在内存中的存放顺序,对于单字符来说是没有字节序问题的,字符串是单字符的集合,字符串也没有字节序问题。

       小端方式:数据低位字节在内存低位地址,计算机主机存放数据方式
       大端方式:数据低位字节在内存高位地址,网络通信存放数据方式
       在网路通信中,主机发送数据前,IP地址和端口号要实现小端方式到大端方式的转换,主机接收数据后,IP地址和端口号要实现大端方式到小端方式的转换。

        h表示host主机,n表示network网络,s表示unsigned short无符号短整形,l表示unsigned int无符号整形

  1. 主机字节序(小端方式) -> 网络字节序(大端方式)

    #include <arpa/inet.h> //头文件中
    
    //uint16_t类型表示unsigned short类型,适用于unsigned short无符号短整形2字节16位的主机端口号
    uint16_t htons(uint16_t hostshort);
    u_short htons (u_short hostshort );(Windows)
    //uint32_t类型表示unsigned int类型,适用于unsigned int无符号整形4字节32位的host主机ip地址
    uint32_t htonl(uint32_t hostlong);
    u_long htonl ( u_long hostlong);(Windows)
  2. 网络字节序(大端方式) -> 主机字节序(小端方式)

    //uint16_t类型表示unsigned short类型,适用于unsigned short无符号短整形16位的网络端口号
    uint16_t ntohs(uint16_t netshort);
    u_short ntohs (u_short netshort );(Windows)
    //uint32_t类型表示unsigned int类型,适用于unsigned long无符号长整形32位的网络ip地址
    uint32_t ntohl(uint32_t netlong);
    u_long ntohl ( u_long netlong);(Windows)

ip地址转换:

        在网络通信中,在主机上通常使用ASCII地址(点分十进制)IP地址字符串(字符串没有字节序问题),但在编程中socket结构体中使用网络字节序的32位无符号整形unsigned int的IP地址,因此要经常实现主机字节序的点分十进制字符串ip地址和网络字节序的32位无符号整形IP地址相互转换
      inet表示IPV4协议,n表示网络,p表示主机点分十进制的IP地址

 #include <arpa/inet.h>//头文件<arpa/inet.h>

1)主机字节序的点分十进制的ip地址字符串 -> 网络字节序的无符号整形unsigned int的ip地址

int inet_pton(int af, const char *src, void *dst);

//成功返回1,失败返回0或-1,可用于IPV4协议和IPV6协议的地址转换
//af: 地址族协议:AF_INET(ipv4格式的ip地址)、AF_INET6(ipv6格式的ip地址)
//src: 传入参数, 对应要转换的点分十进制的主机字节序ip地址字符串,如192.168.1.100
 //dst: 传出参数, 保存src转换后得到的网络字节序in_addr_t类型(32位无符号整形unsigned int)IP地址的内存地址

in_addr_t inet_addr(const char* src);
unsigned long inet_addr (const char FAR * src);(Windows),FAR在16位操作系统指明寻址方式

//和inet_pton函数功能相同但只能用于IPV4地址转换,成功返回网络字节序的32位无符号整形的IPV4地址,失败返回-1


  2) 网络字节序32位无符号整形unsigned int的ip地址 -> 主机字节序的点分十进制的ip地址字符串

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

//成功: 返回保存结果在dst对应的内存地址, 失败: 返回NULL
 //af: 地址族协议:AF_INET(ipv4格式的ip地址)、AF_INET6(ipv6格式的ip地址)
//src: 传入参数, 保存网络字节序的in_addr_t类型(32位无符号整形unsigned int)IP地址的内存地址
 //dst: 传出参数, 存储转换得到的主机字节序的点分十进制的IP地址字符串
//size: 修饰dst参数的, 标记dst指向的内存中可存储的字节数,一般使用宏INET_ADDRSTRLEN,也就是16即可


3)struct in_addr结构体类型-> 主机字节序的点分十进制的ip地址字符串

char* inet_ntoa(struct in_addr in);
char* inet_ntoa(struct in_addr in);(Windows)

 //和inet_ntop功能相同但只能用于IPV4地址转换,成功返回保存转换后的ASCII地址点分十进制字符串的内存地址
 //in:struct in_addr类型,即struct sockaddr_in结构的sin_addr属性,该结构体变量in的成员属性s_addr保存要转换的网络字节序unsigned int的IPV4地址


三、Socket编程核心函数

        Socket套接字作为网络通信的接口,使用其就能完成两台主机(服务器和客户端)进程之间端到端的网络(TCP/UDP)通信,本质为一种文件描述符,可以实现本主机内进程间通信,还可实现网络中不同主机之间通信,Socket套接字相关函数在头文件<sys/socket.h>中

1.create()创建一个遵循某种传输协议的套接字,套接字会自动绑定在创建其的服务进程上:
int socket(int domain, int type, int protocol);
SOCKET socket(int af,int type,int protocal);(Windows),SOCKET类型实质为int类型

//socket函数返回得到的套接字socket本质上是一个内核内存的文件描述符fd,网络通信基于该文件描述符通信,该套接字socket文件描述符可用于监听或通信
//domain: 使用的地址族协议枚举值:AF_INET/PF_INET: 使用IPv4格式的ip地址;AF_INET6/PF_INET6: 使用IPv6格式的ip地址
//type:使用的传输方式枚举值:SOCK_STREAM: 使用流式的传输协议;SOCK_DGRAM: 使用报式(报文)的传输协议
//protocol: 一般写0使用默认的type类型传输协议:SOCK_STREAM: 流式传输默认使用的是tcp;SOCK_DGRAM: 报式传输默认使用的udp
 //返回值:成功返回套接字socket,失败返回-1

2.bind()绑定服务器进程的监听套接字socket文件描述符和本地服务器的IP地址与固定端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);//成功返回0,失败返回-1
int bind(SOCKET sockfd,const struct sockaddr FAR* addr, int addrlen);(Windows)

//sockfd: 监听的套接字socket文件描述符用于绑定本地服务的IP地址和端口号, 即通过socket()调用得到的返回值
 //addr: struct sockaddr类型的传入参数,存储要绑定本地的网络字节序的IP地址和固定端口号,一般先初始化struct sockaddr_in类型后,地址强制转化为struct sockaddr*类型,一般用于服务器进行本地监听地址的绑定,IP地址一般取INADDR_ANY(即"0.0.0.0"),表示服务器本地任一IP地址,端口号选择在1024-49151的任一不冲突的注册端口号。
//addrlen: 参数addr指向的内存大小, 即sizeof(struct sockaddr)可以得到,socklen_t类型为有符号整数类似于int,专门用于套接字长度参数的数据类型。

3.listen()本地服务的监听套接字设置监听客户端的连接:
int listen(int sockfd, int backlog);//成功返回0,失败返回-1
int listen(SOCKET sockfd,int backlog);(Windows)

//sockfd:监听的套接字socket文件描述符,即调用socket()得到,在监听之前必须要绑定 bind()
//backlog:本地服务能一次处理的最大客户端连接要求数量,最大值为128,但可以进行多次监听,因此服务器可以连接的客户端数和一次监听的数量无关

4.accept()阻塞并等待接受客户端的连接请求
//成功返回与该客户端通信的新套接字socket文件描述符,失败返回-1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
SOCKET accept ( SOCKET sockfd, struct sockaddr FAR* addr, int FAR* addrlen );(Windows)

//当没有新的客户端连接请求的时候会阻塞服务进程,当检测到有新的客户端连接请求时阻塞解除,建立新连接得到的返回值是一个可用与客户端通信的新套接字socket文件描述符,基于这个套接字socket文件描述符就可以实现:服务器---->>客户端方向的网络数据通信。
//sockfd: 本地服务绑定的用于监听的套接字socket文件描述符
//addr: 传出参数, 传出存储的建立连接通信客户端的IP地址和端口号信息,并且是网络字节序,如果不需要保存,设置为NULL即可
 //addrlen: 传出参数,传出修改后的存储客户端的IP地址和端口号的addr的内存大小的指针,addr为NULL不保留客户端地址时,设置为NULL即可

5.connect()客户端自动绑定并连接指定服务器服务进程的IP地址和端口号:
/成功返回0,失败返回-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);/
int connect (SOCKET sockfd,const struct sockaddr FAR* addr,int addrlen );(Windows)

//客户端进程调用的connect()函数会自动给进程绑定本地主机IP地址和一个在49152-65535的随机动态端口号,否则需要提前使用bind()函数绑定IP地址和一个固定端口号,但由于服务器不会主动连接客户端,不需要客户端绑定一个固定端口号因此自动绑定即可,connect()函数实现客户端某个进程和服务器服务进程进行连接,进而使用该客户端使用socket()函数创建的用于通信的套接字socket文件描述符实现客户端---->>>服务器方向的网络数据通信。
 //sockfd: 用于通信的套接字socket文件描述符, 由客户端进程通过调用socket()函数得到。
//addr: 存储要连接的服务器端进程服务绑定的的网络字节序的IP地址和端口号,即服务器使用bind()函数绑定本地服务进程的网络字节序的IP地址和固定端口号
//addrlen: 存储addr指针指向的内存的大小,即sizeof(struct sockaddr)

6.read()/recv()通过套接字socket文件描述符读入/接收网络通信的数据:
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
int recv (SOCKET sockfd,char FAR* buf,int size,int flags);(Windows)

//如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞进程并等待数据到达,数据到达后函数解除阻塞进程,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,函数直接返回0,不会阻塞进程。
//返回值:(1)大于0=>接收端实际接收的字节数,;(2)等于0:发送端断开连接;(3)-1:接收端接收数据失败;
//sockfd: 用于通信的套接字socket文件描述符:对于服务器通过该套接字接收客户端数据,使用的socket为accept()函数的返回值对于客户端通过该套接字接收服务器数据,使用的socket为客户端使用socket()函数创建的并进行connect()函数连接的套接字socket文件描述符
//buf: 指向一块有效内存, 用于存储接收的数据
//size: 参数buf指向的内存的容量大小,用于保护buf缓冲区不溢出
//flags: 特殊的属性, 一般不使用, 指定为 0

7.write()/send()通过套接字socket文件描述符写出/发送网络通信的数据:
ssize_t write(int sockfd, const void *buf, size_t len);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
int send (SOCKET sockfd,const char FAR * buf, int size,int flags);(Windows)

//返回值:大于0=》实际发送的字节数,在buf缓冲区中消息数据很长超过TCP数据包时,write函数会分多次发送;-1:发送数据失败
 //sockfd: 用于通信的套接字socket文件描述符:对于服务器通过该套接字给客户端发送数据,使用的socket为accept()函数的返回值 对于客户端通过该套接字给服务器发送数据,使用的socket为客户端使用socket()函数创建的并进行connect()函数连接的套接字socket文件描述符
//buf: 指向一块有效内存, 用于存储要发送的数据
//len:要发送的实际buf缓冲区数据大小,即指明发送数据的字节个数
 //flags: 特殊的属性, 一般不使用, 指定为 0

8.关闭套接字socket文件描述符的网络连接:
int close(int sockfd);//成功返回0,失败返回-1
int closesocket (SOCKET sockfd);(Windows)

//客户端和服务器都要分别使用close()函数关闭单向连接,从而关闭双向连接,由客户端先使用close()函数进行断开连接,服务器才会使用close()函数断开连接。
//sockfd:套接字文件描述符,对于客户端为使用socket()函数创建的并进行connect()函数连接的套接字socket文件描述符,对于服务器为accept()函数的返回值

四、Socket编程的TCP通信流程与UDP通信流程

TCP通信流程

        每个步骤都要有套接字文件描述符,各个步骤依赖上个步骤的套接字文件描述符即可,故也叫socket通信
1)服务器:
                1.使用socket()函数创建流式传输协议的TCP协议的套接字文件描述符用于监听,套接字会自动绑定在创建其的服务进程中=》
                2.使用bind()函数绑定用于监听的socket()函数创建套接字文件描述符和本地服务器进程的IP地址和端口号=》
                3.使用listen()函数给socket()函数创建的套接字文件描述符设置监听,检测客户端的连接=》
                4.使用accept()函数阻塞绑定进程,等待并同意客户端的连接,并返回用于通信的新套接字文件描述符=》
                5.使用read()/recv()函数阻塞进程自身,等待接收服务器进程服务连接的客户端发送的数据,通过accept()函数返回的套接字文件描述符sockfd通信=》
                6.使用write()/send()函数给连接的客户端进程发送数据,通过accept()函数返回的套接字文件描述符sockfd通信=》
                7.使用close()函数在客户端断开连接后,断开连接。
2)客户端:
                1.使用socket()函数创建流式传输协议的TCP协议的套接字文件描述符用于通信=》
                2.使用connect()函数自动绑定进程随机端口号(可提前使用bind()函数绑定固定端口号但没必要),并与指定IP地址和端口号的服务器进程进行连接,通过socket()函数创建的套接字文件描述符。
                3.使用read()/recv()函数/write()/send()函数进行与连接的服务器进程进行数据交换,通过socket()函数创建的用于通信的套接字文件描述符=》
                4.使用close()函数率先发起断开连接请求,使用的是socket()函数创建的用于通信的套接字文件描述符。

UDP通信流程:

        UDP通信流程类似于TCP通信流程,相比更加简单,不需要连接
1)服务器:

                1.使用socket()函数创建报式传输协议的UDP协议的套接字文件描述符用于通信=》
                2.使用bind()函数绑定用于监听的socket()函数创建套接字文件描述符和本地服务器进程的IP地址和端口号=》
                3.使用recvfrom()函数接收客户端发来的UDP数据报文
                4.使用sendto()函数向客户端程序发送处理数据报文后的反馈信息,同样通过数据报文的方式发送。
                5.使用close()函数在客户端关闭与服务器通信后,进行关闭服务器与客户端通信。
2)客户端:
                1.使用socket()函数创建报式传输协议的UDP协议的套接字文件描述符用于通信,系统会给该套接字文件描述符随机分配端口号,并自动绑定客户端进程和本地IP地址和随机分配的端口号,也可使用bind()指定一个固定的端口号绑定进程但没必要=》
                2.使用sendto()函数向服务器发送UDP数据报文。
                3.使用recvfrom()函数接收服务器发送的处理后的数据报文。
                4.使用close()函数率先断开和服务器的通信。

  五、Socket编程的套接字通信工作原理

         1.套接字在本质上是文件描述符,其中对应两块内核管理的内存, 一块内存是读缓冲区, 一块内存是写缓冲区。
                读缓冲区: 进程通过文件描述符将接收数据读出的内存区域, 通过套接字文件描述符的读缓冲区保存发送者发送的数据,等待进程通过套接字文件描述符的读缓冲区将数据读出
                写缓冲区: 进程通过文件描述符将发送数据写入的内存区域, 通过将要发送的数据写入套接字文件描述符的写缓冲区等待发送,发送到接收者的读缓冲区
        2.在tcp的服务器端有两类套接字文件描述符:分别用于监听和通信的套接字文件描述符
                (1)监听的套接字文件描述符:只有一个,不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
                (2)通信的套接字文件描述符:N个客户端和服务器建立新连接, 通信的套接字文件描述符就有N个,每一个套接字文件描述符分别负责服务器和建立连接的客户端通信客户端使用connect()函数将要发送的连接请求通过套接字的写缓冲区发送到服务器监听套接字的读缓冲区中,等待服务器使用accept()函数读出 连接请求并同意,建立服务器与客户端通信的新套接字,服务器服务进程基于该新套接字文件描述符的读缓冲区和写缓冲区与客户端进程进行数据交换。
        3.udp的服务器端只有一类用于通信的套接字文件描述符,服务器进程通过该套接字文件描述符的读缓冲区和写缓冲区与客户端进程进行数据交换。
        4.客户端只有一类文件用于通信的套接字文件描述符:客户端进程通过该套接字的读缓冲区和写缓冲区与服务器服务进程进行数据交换。

 六、TCP服务器要注意的2个问题:

包括以下两个:

  • read/write函数中用户缓冲区buf数据和套接字内核读写缓冲区的交换

        read/write函数是直接分别进行用户缓冲区buf和内核读/写缓冲区进行数据交换,然后通过内核读/写缓冲区将从网络中读取数据或发生到网络。

        1.write函数:无论要发送的用户缓冲区buf数据多长,write仅负责将buf数据拷贝到内核发送缓冲区,此时分为几种情况。
                 ①在默认阻塞情况下:write函数会尽量将用户缓冲区buf数据的指定字节数len全部写入到内核发送缓冲区,如果内核发送缓冲区大小不够,会先写入部分buf数据然后进行阻塞,等到内核发送缓冲区有剩余空间时,唤醒继续写入部分buf数据,直到write写入的字节数达到指定len发送字节数返回实际写入的字节数。这个过程可能会被超时信号等其他信号或各种原因打断,而提前返回实际写入的字节数或-1表示发生错误。
                  ②在非阻塞情况下:write函数会采取不阻塞的前提下,将用户缓冲区buf数据的指定字节数len写入到内核发送缓冲区,如果当前内核发送缓冲区大小不够,会直接写入可以容纳的字节数,然后直接返回实际写入的字节数。在内核发送缓冲区本身就为满时,write函数会直接返回-1,并设置errno为 EAGAIN/EWOULDBLOCK。
                    在上述2种情况后,将buf数据写入发送缓冲区,发送缓冲区的数据是否发送、何时发送由协议栈根据MSS、Nagle算法、滑动窗口等决定,可能会拆分为多个TCP报文分段发送。
              2.read函数:read仅负责将内核接收缓冲区数据读入到用户缓冲区buf中,此时包括以下几种情况:
                   ①在默认阻塞情况下:read函数会直接读取在内核接收缓冲区的字节数和指定请求的字节数中的最小值个数字节数,返回实际读取的字节个数。如果内核接收缓冲区本身就为空时,read函数会直接阻塞,直到对端发送网络数据到内核接收缓冲区解除阻塞,然后执行上述行为。
                   ②在非阻塞情况下:内核接收缓冲区有数据时,socket在默认阻塞或不阻塞的情况下,read函数都会读取内核接收缓冲区的字节数或指定的读取字节数中较小长度的字节数据,并返回实际读取的字节长度。在内核接收缓冲区本身就为空时,会直接返回-1,并设置errno为EAGAIN/EWOULDBLOCK。
        由于TCP协议是面向字节流的流式传输方式,并且没有数据边界,不同TCP包可能混合发送和读取,因此在write函数发送时除发送数据本身,还需要发送数据长度,并还会附带数据ID等额外数据构成头部,在read函数读取时首先读取数据长度等信息,然后循环读取指定数据长度的数据作为一个完整的TCP数据包。
         无论是否采用阻塞socket方式,read/write函数都无法保证一定可以读取/发送指定长度的数据,都要采取循环read/write方式保证。由于发送数据时,发送数据长度是一定知道的,接收数据时,TCP协议是面向字节流的,无法提前知道对面发送的数据字节数,此时往往进行黏包处理,按照某种协议先读取指定字节数获取数据包长度len,然后在循环read读取len个字节数据。

  • read/write函数的返回值及是否断开连接关闭通信套接字socket

        1.write函数正常情况下会返回>0的发送字节数,在错误情况下会返回-1,并设置errno错误码,其中非阻塞情况下,errno为EAGAIN或EWOULDBLOCK表示套接字内核发送缓冲区已满,在阻塞情况下不会返回该错误。在发现返回值为-1,并errno不为EAGAIN或EWOULDBLOCK时,可以考虑直接关闭套接字,errno为EAGAIN或EWOULDBLOCK时,可以进行循环write或其他操作。
        2.read函数正常情况下会返回>0的读取字节数,在对端主动断开连接的情况下会直接返回0,在出现错误情况下会返回-1,并设置errno错误码,在非阻塞情况下,errno为EAGAIN或EWOULDBLOCK表示套接字内核接收缓冲区为空,在阻塞情况下不会返回该错误。在发现返回值为-1,并errno不为EAGAIN或EWOULDBLOCK时或者返回值为0时,可以考虑直接关闭套接字,errno为EAGAIN或EWOULDBLOCK时,可以进行循环write或其他操作。
       3.在长连接的情况下,采取心跳包机制时,在连续3次没有回应时,主动关闭套接字。

注意:】Windows的套接字通信和Linux的套接字通信方式相同,并且api函数和结构体变量类型也几乎一致,但在Windows下套接字函数使用前需要额外包含对应的头文件<winsock2.h>以及加载相应的动态库ws2_32.dll,使用后要释放套接字函数的Winsock相关库。

#include<winsock2.h>//包含头文件
WSAData wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);//加载初始化套接字动态库ws2_32.dll
……………………//使用Windows环境下的套接字通信函数进行Socket编程
WSACleanup();//使用完后注销Winsock相关库

七、简单的服务器与客户端示例代码(详细代码注释)

        以下服务器的实现有几个比较大的问题导致实际生产中无法使用,解决方式后续文章会出,感谢关注支持。
①服务器程序没有采用并发连接通信方式,只能服务几个客户端,并发效率很低。

②数据的接收和发送没有进行“粘包处理”,TCP是没有数据边界的面向字节流,会导致读取的数据可能混合几个TCP包数据,尤其在长数据时。

③服务器与客户端的通信是同步的,不能做到客户端和服务器想发就发方式。

1.综合进程等Linux系统编程的服务器与客户端TCP示例代码

        适用于有Linux系统编程基础的伙伴,可以拷贝学习,关于Linux系统编程相关知识的干货总结在后续文章会出,感谢支持关注。

        基本思想:采用主进程创建服务器程序运行,子进程创建客户端,并在子进程中创建多个子进程模拟多并发连接服务器的客户端,遵循服务器与客户端通信的基本流程。

#include <stdio.h>//提供用于输入输出的函数,如printf()、scanf()、fprintf()、fscanf(),包含了文件操作的一些函数,如fopen()、fclose()、fread()、fwrite()等。
#include <stdlib.h>//提供各种通用的工具函数,如内存分配(malloc()、calloc()、realloc()、free())、随机数生成(rand()、srand())、环境查询(getenv())、程序控制(exit()、system())。
#include <unistd.h>//提供对POSIX操作系统API的访问包括sleep函数,主要用于Unix-like系统(如Linux、macOS),在Windows系统上不可用,因为它是Unix特有的。
#include <string.h>//提供用于处理C风格字符串(即以'\0'结尾的字符数组)的函数,字符串复制(strcpy())、连接(strcat())、比较(strcmp())、长度计算(strlen())等函数。
// 线程相关
#include <pthread.h>//提供了一套创建和管理线程以及线程间同步的机制,使得开发者能够在Unix-like系统(如Linux和macOS)上实现多线程编程,具体实现在动态库libpthread.so中
#include <semaphore.h> //包括信号量sem_t
// 进程相关
#include <sys/resource.h> // 进程优先级相关函数
#include <sys/wait.h> // 进程等待函数
#include <sys/types.h> // 类型定义
#include <signal.h> // 信号函数
// socket网络编程相关
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数

// 简单非并发的服务器和客户端示例
// 采用主进程用于创建服务器,子进程创建多个客户端线程进行通信
bool flag = true; // 定义主线程服务器运行标志位,true表示运行,false表示终止,默认是运行状态
sigjmp_buf env; // 保存服务器进程堆栈状态
void simple_server_client() {
    // 创建信号量集实现主进程服务器和子进程客户端进行先后同步关系,避免出现客户端先运行,服务器后运行,直接出现问题
    int semmid = semget(ftok(".", 1), 1, 0666| IPC_CREAT); // 创建信号量
    if (semmid == -1)
        return; // 创建失败直接返回
    semctl(semmid, 0, SETVAL, 0); // 信号量初始值设置为0

    // 主进程回收终止子进程的信号函数
    struct sigaction sigact_chld;
    sigact_chld.sa_handler = [](int signal) {
        switch (signal) {
        case SIGCHLD:
            // 子进程终止信号,非阻塞回收子进程的子进程
            int cpid;
            while ((cpid = waitpid(-1, NULL, WNOHANG)) > 0)
                printf("recycle son process:%d\n", cpid);
            break;

        default:
            break;
        }
    };
    sigact_chld.sa_flags = SA_RESTART;
    sigemptyset(&sigact_chld.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
    // 注册信号和信号处理函数
    sigaction(SIGINT, &sigact_chld, NULL);
        
    int pid = fork(); // 创建子进程
    if (pid > 0) {
        // 主进程
        printf("main process:%d start server!\n", getpid());

        // lambda表达式创建清理函数,用于退出前打印信息,回收子进程,并是否执行清理套接字操作
        void (*clear)(const char*, int) = [](const char* tips, int sockfd = -1) {
            // 打印提示的错误信息tips
            printf("%s\n", tips);

            // 判断是否清理已经创建的套接字sockfd
            if (sockfd > 0)
                close(sockfd);

            // 退出主进程先非阻塞回收所有已终止的子线程,然后退出主线程,还在运行的子线程退出交给内核管理
            int pid; // 保存子进程pid
            int status; // 子进程状态
            while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
                printf("子进程%d退出,退出状态%d!\n", pid, WEXITSTATUS(status));
        };

        // 主进程信号回调函数,注册ctrl+c的SIGINT信号和回调处理函数为终止服务器运行
        struct sigaction sigact;
        sigact.sa_handler = [](int signal) {
            if (signal == SIGINT) {
                flag = false;// 终止服务器运行,设置服务器运行标志位false
                // 直接跳转恢复堆栈状态,并传出SIGINT信号
                siglongjmp(env, signal);
            }
        };
        sigact.sa_flags = SA_RESTART;
        sigemptyset(&sigact.sa_mask); // 信号屏蔽位置空,默认只屏蔽自身
        // 注册信号和信号处理函数
        sigaction(SIGINT, &sigact, NULL);


        // 开始socket编程
        // 1.主进程服务器创建用于监听的套接字socket
        int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        // 创建socket失败,直接执行清理函数退出
        if (listen_sockfd == -1) {
            char buf[64];
            sprintf(buf, "server:%d socket() error!", getpid());
            clear(buf, listen_sockfd);// 退出函数
            return;
        }

        // 2.主进程服务器绑定监听套接字和本地地址
        struct sockaddr_in sockaddr;
        sockaddr.sin_family = AF_INET; // 指定协议族IPV4
        sockaddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 点分十进制的0.0.0.0表示本地任意地址
        sockaddr.sin_port = 10086; // 选择任意一个注册端口号
        int ret = bind(listen_sockfd, (struct sockaddr*)&sockaddr, sizeof(struct sockaddr));
        // 绑定bind失败,直接执行清理函数退出
        if (ret == -1) {
            char buf[64];
            sprintf(buf, "server:%d bind() error!", getpid());
            clear(buf, listen_sockfd); // 退出函数
            return;
        }

        //sleep(5);
        // 3.主进程服务器进行监听客户端的连接
        ret = listen(listen_sockfd, 16); 
        // 监听listen失败,直接执行清理函数退出
        if (ret == -1) {
            char buf[64];
            sprintf(buf, "server:%d listen() error!", getpid());
            clear(buf, listen_sockfd); // 退出函数
            return;
        }
        printf("server:%d listening(port:%d)........\n", getpid(), sockaddr.sin_port);
        // 服务器主进程初始化完毕,执行信号量V操作唤醒子进程客户端
        struct sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_op = +1; // V操作
        sem_buf.sem_flg = 0; // 默认采取阻塞方式
        semop(semmid, &sem_buf, 1); // 执行V操作

        // 循环接收客户端的连接,并进行排队通信,这里需要并发处理
        while (flag) {
            // 保存堆栈状态,保证在ctrl+c时跳转后,直接终止服务器循环
            ret = sigsetjmp(env, 1); // 保存堆栈状态
            if (ret)
                break; // 堆栈返回值出现错误时,直接退出服务器

            // 4.接收客户段的连接
            struct sockaddr_in client_addr; // 传出参数,用于保存连接的客户端地址信息
            socklen_t len = sizeof(client_addr); // 传出参数,client_addr长度
            int communciate_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &len);
            // 检查返回的通信套接字成功,失败直接退出与该客户端的连接,成功则继续进行通信
            if (communciate_sockfd == -1)
                continue; // 直接进行下个客户端的连接
            printf("server connect client:%s:%d success!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

            // 服务器与本连接成功的客户端循环通信,假设是一个长连接的场景
            while(1){
                // 5.服务器等待客户端的信息,这里需要粘包处理
                char rdbuf[1024]; // 读取缓冲区
                // 采用read函数等待读取客户端发送数据
                int rdbyte = read(communciate_sockfd, rdbuf, sizeof(rdbuf));
                // 根据返回值判断状态,执行相应操作
                if (rdbyte == -1) {
                    // read函数返回-1,表示连接出现错误,直接关闭和客户端的通信
                    printf("server connect client:%s:%d error, server disconnect!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                    close(communciate_sockfd);
                    break; // 直接进行下个客户端的连接
                }
                else if (rdbyte == 0) {
                    // read函数返回0,表示客户端断开连接,直接关闭和客户端的通信
                    printf("client:%s:%d disconnect, server disconnect!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                    close(communciate_sockfd);
                    break; // 直接进行下个客户端的连接
                }
                else
                    printf("server receive client:%s:%d numbers %d data:%s.\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), rdbyte, rdbuf);
                // 清理读缓冲区数据,以避免影响下次数据的接收
                memset(rdbuf, 0, sizeof(rdbuf));

                sleep(1);

                // 6.服务器成功读取客户端的信息,进行处理然后使用发送数据给客户端
                char wrbuf[] = "Hello World form server"; // 服务器发送给客户端的数据
                int wrbyte = write(communciate_sockfd, wrbuf, sizeof(wrbuf));
                // 根据返回值判断状态,执行相应操作
                if (wrbyte == -1) {
                    // write函数返回-1,表示连接出现错误,直接关闭和客户端的通信
                    printf("server connect client:%s:%d error, server disconnect!\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                    close(communciate_sockfd);
                    break; // 直接进行下个客户端的连接
                }
                else
                    printf("server send client:%s:%d number %d data.\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), sizeof(wrbuf));
            }

            // 由于是主进程,还要周期性的非阻塞回收当前已终止的子进程,并保证主进程运行,不阻塞主进程
            int spid;
            while ((spid = waitpid(-1, NULL, WNOHANG)) > 0)
                printf("main process recycle son process:%d\n", spid);
        }
        // 服务器运行结束前,清理资源
        close(listen_sockfd); // 关闭监听套接字
    
        // 由于是主进程,主进程运行结束前,阻塞回收所有子进程
        int spid;
        while ((spid = waitpid(-1, NULL, 0)) > 0) 
            printf("main process recycle son process:%d\n", spid);

        // 主进程创建的信号量在所有子进程结束后才回收信号量
        semctl(semmid, 0, IPC_RMID); // 删除信号量
        // 主进程服务器运行结束
    }
    else {
        // 执行信号量的P操作,避免出现客户端子进程比服务器先运行的情况
        sembuf sem_buf;
        sem_buf.sem_num = 0;
        sem_buf.sem_op = -1; // P操作
        sem_buf.sem_flg = 0; // 默认阻塞方式
        semop(semmid, &sem_buf, 1); // 执行P操作

        // 子进程模拟创建多个并发的多进程客户端进行与服务器进行通信
        int spid; // 子进程fork的返回值
        for (int i = 0; i < 1; i++) {
            spid = fork(); // 创建子进程
            if (spid == 0)
                break; // 保证不会使创建的子进程迭代创建,只有父进程进行子进程的创建
        }

        // 子进程作为客户端访问服务器
        printf("son process:%d start client!\n", getpid());
        // 1.创建与服务器进行通信的客户端socket
        int communicate_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (communicate_sockfd == -1) {
            printf("client:%d socket() error!\n", getpid());
            return;
        }

        // 2.客户端连接服务器,需要和服务器bind绑定的地址相同
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = inet_addr("192.168.137.3"); // 查看自己主机网口的所有IP地址的任意一个,其中127.0.0.1是必有的
        server_addr.sin_port = 10086;
        int ret = connect(communicate_sockfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
        if (ret == -1) {
            printf("client:%d connect() error!\n", getpid());;
            close(communicate_sockfd);
            return;
        }

        // 客户端和服务器进行循环通信
        // 假设客户端和服务器只通信2次,客户端就主动关闭
        for (int i = 0; i < 2; i++) {
            // 3.客户端给服务器发送数据
            char wrbuf[1024];
            sprintf(wrbuf, "Hello World form client:%d", getpid());
            int wrbyte = write(communicate_sockfd, wrbuf, sizeof(wrbuf));
            // 根据返回值判断状态,执行相应行为
            if (wrbyte == -1) {
                printf("client:%d connect server error, client disconnect!\n", getpid());
                close(communicate_sockfd);
                break;
            }
            else
                printf("client:%d send numbers:%d data.\n", getpid(), sizeof(wrbuf));

            sleep(1);

            // 4.客户端等待接收服务器的数据
            char rdbuf[1024];
            int rdbyte = read(communicate_sockfd, rdbuf, sizeof(rdbuf));
            // 根据返回值判断状态,执行相应行为
            if (rdbyte == -1) {
                printf("client:%d connect server error, client disconnect!\n", getpid());
                close(communicate_sockfd);
                break;
            }
            else if (rdbyte == 0) {
                printf("server disconnect, client:%d disconnect!\n", getpid());
                close(communicate_sockfd);
                break;
            }
            else
                printf("client:%d receive server numbers:%d data:%s\n", getpid(), rdbyte, rdbuf);
            // 清理读缓冲区数据,以避免影响下次数据的接收
            memset(rdbuf, 0, sizeof(rdbuf));
        }

        // 客户端主动关闭套接字,关闭与服务器连接
        close(communicate_sockfd);

        // 主进程退出前非阻塞回收所有以终止子进程,其他子进程交给内核处理
        if (spid > 0) {
            // 阻塞回收子进程的子进程
            int cpid;
            while ((cpid = waitpid(-1, NULL, 0)) > 0)
                printf("client son process recycle son process:%d\n", cpid);
        }
        return;

        // 客户端子进程运行结束
    }

}

2.简单的socket编程TCP服务器和客户端示例代码

基本思想:服务器与客户端分别在两个程序中,分别遵循各自的通信流程。

服务器程序:

/*c语言实现一个单线程的服务器程序
服务器:
    1.使用socket()函数创建流式传输协议的TCP协议的套接字文件描述符用于监听,套接字会自动绑定在创建其的服务进程中=》
    2.使用bind()函数绑定用于监听的socket()函数创建套接字文件描述符和本地服务器进程的IP地址和端口号=》
    3.使用listen()函数给socket()函数创建的套接字文件描述符设置监听,检测客户端的连接=》
    4.使用accept()函数阻塞绑定进程,等待并同意客户端的连接,并返回用于通信的新套接字文件描述符=》
    5.使用read()/recv()函数阻塞进程自身,等待接收服务器进程服务连接的客户端发送的数据,通过accept()函数返回的套接字文件描述符sockfd通信=》
    6.使用write()/send()函数给连接的客户端进程发送数据,通过accept()函数返回的套接字文件描述符sockfd通信=》
    7.使用close()函数在客户端断开连接后,断开连接。
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h> //Socket编程的数据结构和函数
#include <unistd.h>

int main()
{
    //1.创建用于监听的套接字文件描述符,套接字会自动绑定在创建其的服务进程中
    int sockfd_listen = socket(AF_INET, SOCK_STREAM, 0); //创建一个IPV4协议,流式传输TCP协议的套接字文件描述符
    if (sockfd_listen == -1)                             //当socket()函数创建套接字失败时,返回值为-1
    {
        perror("socket");
        return -1;
    }
    printf("服务器创建的用于监听的套接字文件描述符为:%d\n", sockfd_listen);

    //2.绑定监听的套接字文件描述符和IP地址及固定端口号
    //先准备服务器本地的IP地址和固定端口号初始化到结构体struct sockaddr_in变量中,再将地址强制转换成结构体struct sockaddr*类型地址到bind()函数中。
    struct sockaddr_in saddr_in_server;                             //保存本地服务器的IP地址和端口号信息
    saddr_in_server.sin_family = AF_INET;                           //初始化IPV4协议
    saddr_in_server.sin_addr.s_addr = inet_addr("192.168.119.101"); //必须将主机字节序的SACII码地址转化成网络字节序的无符号整形的32位IP地址
    saddr_in_server.sin_port = 10001;                               //本身就是网络字节序,无需使用htons()函数进行转化
    //saddr_in_server.sin_port = htons(10001);                        //固定端口号为10001,必须将主机字节序的无符号短整形的16位端口号转化成网络字节序的无符号短整形的16位端口号
    //struct sockaddr saddr = (struct sockaddr)saddr_in_server;//变量直接强制转换报错,地址强制转换(struct sockaddr *)&saddr_in_server不报错
    int ret = bind(sockfd_listen, (struct sockaddr *)&saddr_in_server, sizeof(struct sockaddr));
    if (ret == -1)//bind()函数绑定失败时返回-1
    { 
        perror("bind");
        return -1;
    }
    printf("该服务器进程绑定的IP地址为:%ld,端口号为:%d\n", saddr_in_server.sin_addr.s_addr, saddr_in_server.sin_port);

    //3.给绑定IP地址和端口号的服务器进程设置监听,检测客户端的连接
    ret = listen(sockfd_listen, 128);
    if (ret == -1) //监听失败返回-1
    {
        perror("listen");
        return -1;
    }
    printf("服务器服务进程正在进行监听,检测客户端进程的连接,,,,,,\n");

    //4.阻塞并接受客户端进程的连接请求
    //先准备保存连接的客户端IP地址和端口号的struct sockaddr结构体类型的传入参数,socklen_t类型的传入传出参数保存变量字节数
    struct sockaddr_in addr_in_client;      //传出参数,用于保存连接的客户端的IP地址及端口号
    socklen_t len = sizeof(addr_in_client); //传入传出参数,保存变量的字节数
    int sockfd_connect = accept(sockfd_listen, (struct sockaddr *)&addr_in_client, &len);
    if (sockfd_connect == -1) //接受连接失败返回-1
    {
        perror("accept");
        return -1;
    }
    printf("连接的客户端进程的IP地址为:%ld,端口号为:%d\n", addr_in_client.sin_addr.s_addr, addr_in_client.sin_port);

    //5.连接成功后,阻塞进程自身等待接收服务器进程服务连接的客户端发送的数据,使用accept()返回的新套接字文件描述符用于与客户端通信
    int number = 0;
    while (1) //循环服务器与客户端进行通信,直到客户端断开连接
    {
        number++;
        //接收客户端数据,recv/read函数具有阻塞作用
        char recv_buf[1024];                                     //准备好要接收客户端发送的数据在缓冲区recv_buf
        int recv_size = recv(sockfd_connect, recv_buf, 1024, 0); //接收数据要指明缓冲区可以接收的最大字节数,以防止溢出
        if (recv_size > 0)                                       //>0,连接成功并接收到客户端发送的数据字节数为recv_size
            printf("服务器接收到客户端发送的%d个字节的数据:%s,…………%d\n", recv_size, recv_buf, number);
        else if (recv_size == 0)                            //==0,客户端断开连接
        { 
            printf("客户端已经断开连接!\n");
            break;//退出循环
        }
        else                                                     //<0,发送数据失败
            perror("recv");

        //6.给客户端发送数据,send/write函数
        char send_buf[] = "hello client!";                                   //准备好要发送给客户端的数据在缓冲区buff保存
        int send_size = send(sockfd_connect, send_buf, sizeof(send_buf), 0); //发送数据需要使用指明发送数据的字节个数
        if (send_size > 0)
            printf("服务器给客户端发送%d个字节的数据,…………%d\n", send_size, number);
        else
            perror("send");

        sleep(1); //休眠1秒
    }

    //7.客户端断开连接后,使用close()函数关闭与客户端的连接
    close(sockfd_listen);  //单线程服务器,关闭监听的套接字文件描述符
    close(sockfd_connect); //关闭通信的套接字文件描述符

    return 0;
}

客户端程序:

/*c语言实现一个单线程的客户端程序
客户端:
    1.使用socket()函数创建流式传输协议的TCP协议的套接字文件描述符用于通信=》
    2.使用connect()函数自动绑定进程随机端口号(可提前使用bind()函数绑定固定端口号但没必要),并与服务器进程进行连接,通过socket()函数创建的套接字文件描述符。
    3.使用write()/send()函数给连接的服务器进程发送数据,通过socket()函数创建的用于通信的套接字文件描述符=》
    4.使用read()/recv()函数接收的服务器进程处理数据的结果,通过socket()函数创建的用于通信的套接字文件描述符,根据recv()/read()函数的返回值不同进行相应操作=》
    5.使用close()函数率先发起断开连接请求,使用的是socket()函数创建的用于通信的套接字文件描述符。
*/

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

int main()
{
    //1.创建用于通信的套接字文件描述符,套接字会自动绑定在创建其的服务进程中
    int sockfd_connect = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd_connect == -1) //返回-1,创建失败
    {
        perror("socket");
        return -1;
    }
    printf("客户端创建的用于通信的套接字文件描述符为:%d\n", sockfd_connect);

    //2.自动绑定客户端进程和客户端本地IP地址和随机端口号,并与服务器进程服务进行连接
    //先准备好要连接的服务器进程的IP地址和端口号的数据结构
    struct sockaddr_in saddr_in_server; //保存服务器的IP地址和端口号信息
    saddr_in_server.sin_family = AF_INET;
    saddr_in_server.sin_addr.s_addr = inet_addr("192.168.119.101"); //必须将主机字节序的SACII码地址转化成网络字节序的无符号整形的32位IP地址
    saddr_in_server.sin_port = 10001;                               //本身就是网络字节序,无需使用htons()函数进行转化
    //saddr_in_server.sin_port = htons(10001);                        //必须将主机字节序的无符号短整形的16位端口号转化成网络字节序的无符号短整形的16位端口号
    int ret = connect(sockfd_connect, (struct sockaddr *)&saddr_in_server, sizeof(struct sockaddr));
    if (ret == -1) //连接失败返回-1
    {
        perror("connect");
        return -1;
    }

    //3.连接成功后,与服务器进行通信
    int number = 0;
    while (1) //循环通信直到结束,客户端主动断开和服务器的连接s
    {
        number++;
        //给服务器发送数据
        char send_buff[] = "hello server!";                                    //准备好要发送的数据保存在缓冲区send_buff
        int send_size = send(sockfd_connect, send_buff, sizeof(send_buff), 0); //发送数据需要使用指明发送数据的字节个数
        if (send_size > 0)
            printf("客户端给服务器发送%d个字节数据,…………%d\n", send_size, number);
        else
            perror("send");

        //接收服务器的数据
        char recv_buff[1024];
        int recv_size = recv(sockfd_connect, recv_buff, 1024, 0); //接收数据要指明缓冲区可以接收的最大字节数1024
        if (recv_size > 0)
            printf("客户端接收服务器发送的%d个字节数据:%s,…………%d\n", recv_size, recv_buff, number);
        else if (recv_size == 0)
        {
            printf("服务器已经断开连接!\n");
            break;
        }
        else
            perror("recv");

        sleep(1); //休眠1秒
    }

    //4.客户端主动断开连接后,关闭用于连接的套接字文件描述符
    close(sockfd_connect);

    return 0;
}

关于UDP的客户端和服务器后续文章会出,谢谢大家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值