实现基于TCP的服务端/客户端

服务端套接字创建过程

第一步:调用socket函数创建套接字

//成功时返回文件表述符,失败时返回-1
int socket(int __domain, int __type, int __protocol)
  • domain:套接字使用的协议族(Protocol Family)信息;
  • type:套接字数据传输类型信息;
  • protocol:计算机间通信中使用的协议信息;

协议族(Protocol Family)

 Name         Purpose                                    Man page
 AF_UNIX      Local communication                        unix(7)
 AF_LOCAL     Synonym for AF_UNIX
 AF_INET      IPv4 Internet protocols                    ip(7)
 AF_IPX       IPX - Novell protocols
 AF_INET6     IPv6 Internet protocols                    ipv6(7)
 AF_PACKET    Low-level packet interface                 packet(7)

套接字类型(Type):是指套接字的传输方式

  • 面向连接的套接字(SOCK_STREAM)
    • 传输过程中数据不会消失
    • 按序传输数据
    • 传输的数据不存在数据边界(Boundary):调用了三次write传递了100字节,接受者仅一次read接收了全部
  • 面向消息的套接字(SOCK_DGRAM)
    • 强调快速传输而非传输顺序
    • 传输的数据可能丢失也可能损毁
    • 传输的数据有数据边界
    • 限制每次传输的数据大小

协议的最终选择

/* Standard well-defined IP protocols.  */
enum
  {
    IPPROTO_IP = 0,	   /* Dummy protocol for TCP.  */
#define IPPROTO_IP		IPPROTO_IP
    IPPROTO_ICMP = 1,	   /* Internet Control Message Protocol.  */
#define IPPROTO_ICMP		IPPROTO_ICMP
    IPPROTO_IGMP = 2,	   /* Internet Group Management Protocol. */
#define IPPROTO_IGMP		IPPROTO_IGMP
    IPPROTO_IPIP = 4,	   /* IPIP tunnels (older KA9Q tunnels use 94).  */
#define IPPROTO_IPIP		IPPROTO_IPIP
    IPPROTO_TCP = 6,	   /* Transmission Control Protocol.  */
#define IPPROTO_TCP		IPPROTO_TCP
    IPPROTO_EGP = 8,	   /* Exterior Gateway Protocol.  */
#define IPPROTO_EGP		IPPROTO_EGP
    IPPROTO_PUP = 12,	   /* PUP protocol.  */
#define IPPROTO_PUP		IPPROTO_PUP
    IPPROTO_UDP = 17,	   /* User Datagram Protocol.  */
#define IPPROTO_UDP		IPPROTO_UDP
    IPPROTO_IDP = 22,	   /* XNS IDP protocol.  */
#define IPPROTO_IDP		IPPROTO_IDP
    IPPROTO_TP = 29,	   /* SO Transport Protocol Class 4.  */
#define IPPROTO_TP		IPPROTO_TP
    IPPROTO_DCCP = 33,	   /* Datagram Congestion Control Protocol.  */
#define IPPROTO_DCCP		IPPROTO_DCCP
    IPPROTO_IPV6 = 41,     /* IPv6 header.  */
#define IPPROTO_IPV6		IPPROTO_IPV6
    IPPROTO_RSVP = 46,	   /* Reservation Protocol.  */
#define IPPROTO_RSVP		IPPROTO_RSVP
    IPPROTO_GRE = 47,	   /* General Routing Encapsulation.  */
#define IPPROTO_GRE		IPPROTO_GRE
    IPPROTO_ESP = 50,      /* encapsulating security payload.  */
#define IPPROTO_ESP		IPPROTO_ESP
    IPPROTO_AH = 51,       /* authentication header.  */
#define IPPROTO_AH		IPPROTO_AH
    IPPROTO_MTP = 92,	   /* Multicast Transport Protocol.  */
#define IPPROTO_MTP		IPPROTO_MTP
    IPPROTO_BEETPH = 94,   /* IP option pseudo header for BEET.  */
#define IPPROTO_BEETPH		IPPROTO_BEETPH
    IPPROTO_ENCAP = 98,	   /* Encapsulation Header.  */
#define IPPROTO_ENCAP		IPPROTO_ENCAP
    IPPROTO_PIM = 103,	   /* Protocol Independent Multicast.  */
#define IPPROTO_PIM		IPPROTO_PIM
    IPPROTO_COMP = 108,	   /* Compression Header Protocol.  */
#define IPPROTO_COMP		IPPROTO_COMP
    IPPROTO_SCTP = 132,	   /* Stream Control Transmission Protocol.  */
#define IPPROTO_SCTP		IPPROTO_SCTP
    IPPROTO_UDPLITE = 136, /* UDP-Lite protocol.  */
#define IPPROTO_UDPLITE		IPPROTO_UDPLITE
    IPPROTO_MPLS = 137,    /* MPLS in IP.  */
#define IPPROTO_MPLS		IPPROTO_MPLS
    IPPROTO_RAW = 255,	   /* Raw IP packets.  */
#define IPPROTO_RAW		IPPROTO_RAW
    IPPROTO_MAX
  };

第二步:调用bind函数分配IP地址和端口号

//成功时返回0,失败时返回-1
int bind(int __fd, const struct sockaddr *__addr, socklen_t __len)
  • __fd:要分配地址信息(IP地址和端口号)的套接字文件表述符。
  • __addr:存有地址信息的结构体变量地址值
  • __len:第二个结构体变量的长度

第三步:调用listen函数转为可接收请求状态

//成功时返回0,失败时返回-1
int listen(int __fd, int __n)
  • __fd:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)。
  • __n:连接请求等待队列的长度。

第四步:调用accept函数受理连接请求

//成功时返回0,失败时返回-1
int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
  • __fd:服务器套接字的文件描述符。
  • __addr:保存发起连接请求的客户端地址信息地变量地址值,调用函数后向传递来的地址变量参数填充客户端的地址信息。
  • __addr_len:第二个参数__addr结构体的长度,但是存有长度的变量地址。函数调用完后,该变量即被填入客户端地址长度。

客户端创建套接字过程

第一步:调用socket函数创建套接字

//成功时返回文件表述符,失败时返回-1
int socket(int __domain, int __type, int __protocol)

第二步:调用socket函数向服务器端发送连接请求

//成功时返回0,失败时返回-1
int connect(int __fd, const struct sockaddr *__addr, socklen_t __len)
  • __fd:客户端套接字文件表述符。
  • __addr:保存目标服务器端地址信息的变量地址值。
  • __len:以字节为单位传递已传递给第二个结构体参数__addr地址变量长度。

客户端调用connect函数后服务端接收连接请求(记录到等待队列)或发生断网等异常情况而中断连接请求才会返回(完成函数调用)。客户端的IP地址和端口号在调用connect函数时由内核自动分配,无需调用标记的bind函数进行分配。
在这里插入图片描述
总体流程就是:服务器端创建套接字后联系调用bind、listen函数进入等待状态,客户端通过调用connect函数发起连接请求,需要注意的是,客户端只能等到服务器端调用listen函数后才能调用connect函数。同时要清楚,客户端调用connect前,服务器端可能先调用了accept函数。当然,此时服务器端在调用accept函数时进入了阻塞状态,直到客户端调用connect函数为止。

TCP套接字中的I/O缓冲

在这里插入图片描述

  • I/O缓冲在每个TCP套接字中单独存在;
  • I/O缓冲在创建套接字时自动生成;
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据;
  • 关闭套接字将丢失输入缓冲中的数据;

编写

echosrv.c

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

int main(int argc, char** argv) {
    // 1. 创建套接字
    int listenfd;
                          //协议族    套接字类型    协议类型
    if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror("socket");
    }

    // 2. 分配套接字地址
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    // inet_aton("127.0.0.1", &servaddr.sin_addr);

    int on = 1;
    // 确保time_wait状态下同一端口仍可使用
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0)
    {
        perror("setsockopt");
    }

    // 3. 绑定套接字地址
    if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof servaddr) < 0) {
        perror("bind");
    }
    // 4. 等待连接请求状态
    if (listen(listenfd, SOMAXCONN) < 0) {
        perror("listen");
    }
    // 5. 允许连接
    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof peeraddr;
    int connfd;
    if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
        perror("accept");
    }

    printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
    printf("port = %d\n", ntohs(peeraddr.sin_port));

    // 6. 数据交换
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof recvbuf);
        int ret = read(connfd, recvbuf, sizeof recvbuf);
        if (ret == 0)
        {
            printf("client close\n");
            break;
        } else if (ret == -1)
        {
            perror("read");
        }
        fputs(recvbuf, stdout);
        write(connfd, recvbuf, ret);
    }

    // 7. 断开连接
    close(connfd);
    close(listenfd);
    return 0;
}

echocli.c

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

int main()
{
    // 1. 创建套接字
    int sockfd;
    if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        perror("socket");
    }

    // 2. 分配套接字地址
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(6666);
    // servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
     servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    // inet_aton("127.0.0.1", &servaddr.sin_addr);

    // 3. 请求链接
    if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
        perror("connect");
    }

    // 4. 数据交换
    char recvbuf[1024] = {0};
    char sendbuf[1024] = {0};
    while (fgets(sendbuf, sizeof sendbuf, stdin) != NULL)   // 键盘输入获取
    {
//        memset(recvbuf, 0, sizeof recvbuf);
//        memset(sendbuf, 0, sizeof sendbuf);
        write(sockfd, sendbuf, sizeof sendbuf); // 写入服务器
        int ret = read(sockfd, recvbuf, sizeof recvbuf);    // 服务器读取
        if (ret == 0)
        {
            printf("server close\n");
            break;
        } else if (ret == -1)
        {
            perror("read");
        }
        fputs(recvbuf, stdout); // 服务器返回数据输出

        // 清空
        memset(recvbuf, 0, sizeof recvbuf);
        memset(sendbuf, 0, sizeof sendbuf);
    }

    // 5. 断开连接
    close(sockfd);

    return 0;
}
posted @ 2021-08-05 10:14  放飞梦想C  阅读(157)  评论(0编辑  收藏  举报