Unix/Linux网络编程(1)——简单的TCP客户服务器模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shanghairuoxiao/article/details/78536133

网络编程是通过网络协议实现两个进程之间的通信。在学习网络编程前建议先学习TCP/IP协议,推荐《图解TCP/IP》和《TCP/IP详解 卷一》这两本书。

网络编程主要是采用客户服务器模型,简称为CS模型。实现网络编程通过套接字API来实现,客户端和服务端都有一个固定的模式。这篇博客先固定框架中使用到的套接字API然后给出一个简单的回射服务器模型。

TCP的Client/Server模式

在TCP/IP协议中已经讲解了TCP协议中三次握手和四次握手过程,以及发送消息和接受消息。那么在linux系统中,内核中已经将这些协议实现,现在我们一起看看linux下套接字编程的API。

TCP服务器端

1. 创建套接字

 #include <sys/socket.h>
 int socket(int family,int type,int protocol);    
      返回:非负描述字---成功   -1---失败

第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);如果套接口类型不是原始套接口,那么第三个参数就为0。

2.绑定套接字
把一个套接字地址(本机IP和端口号)绑定到创建的套接字上。绑定套接字时可以选择指定IP地址和端口,也可以不指定。通配的IP地址用INADDR_ANY表示,通配的端口用0表示,通配的情况下由内核为其指定相应的IP地址和端口号。
对于客户端可以绑定套接字,但是一般不需要,因为客户端的端口号只是临时的,由内核来分配更合理。但是对服务器而言,一般要使用知名端口号,如果不进行绑定,客户端不知道目的端口号,连接不能完成。
这里写图片描述
通配地址实现:htonl(INADDR_ANY)
通配地址,内核将等到套接字已连接TCP或已经发出数据报(UDP)时才指定。

#include <sys/socket.h>  
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
 返回:0---成功   -1---失败 

3.监听
socket创建的套接字是主动套接字,调用listen后变成监听套接字。TCP状态有CLOSE跃迁到LISTEN状态。
backlog是已完成队列和未完成队列大小之和,对于监听套接字有两个队列,一个是未完成队列,一个是已完成队列。

  • 未完成队列:客户端发送一个SYN包,服务器收到后变成SYN_RCVD状态,这样的套接字被加入到未完成队列中。
  • 已完成队列:TCP已经完成了3次握手后,将这个套接字加入到已完成队列,套接字处于ESTABLISHED状态。
    这里写图片描述

下图中可以看出,TCP的三次握手是在调用connect函数时完成的,服务器端没有调用函数,但是必须有套接字在某个端口监听,不然会返回客户端RST,终止连接。
这里写图片描述

#include<sys/socket.h>
int listen(int sockfd, int backlog);

调用listen函数后的套接字称为监听套接字。

4.accept函数
accept函数从已完成连接的队列中取走一个套接字,如果该队列为空,则accept函数阻塞。accept函数的返回值称为已连接套接字,已连接的套接字就建立一个完整的TCP连接,源IP地址,源端口号,目的IP地址,目的端口号都是唯一确定了。

#include <sys/socket.h>         
int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);  

5.数据传输

  • write和read函数:当服务器和客户端的连接建立起来后,就可以进行数据传输了,服务器和客户端用各自的套接字描述符进行读/写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读/写函数write()和read()进行接收和发送操作。

write()函数用于数据的发送

#include <unistd.h>         
 int write(int sockfd, char *buf, int len); 
  回:非负---成功   -1---失败

参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符;参数buf是指向一个用于发送信息的数据缓冲区;len指明传送数据缓冲区的大小。

read()函数用于数据的接收

#include <unistd.h>         
 int read(int sockfd, char *buf, intlen);  
  回:非负---成功   -1---失败

参数sockfd是套接字描述符,对于服务器是accept()函数返回的已连接套接字描述符,对于客户端是调用socket()函数返回的套接字描述符;参数buf是指向一个用于接收信息的数据缓冲区;len指明接收数据缓冲区的大小。

  • send和recv函数:TCP套接字提供了send()和recv()函数,用来发送和接收操作。这两个函数与write()和read()函数很相似,只是多了一个附加的参数。
    (1)send()函数用于数据的发送。
#include <sys/types.h>
#include < sys/socket.h >         
ssize_t send(int sockfd, const void *buf, size_t len, int flags);  
回:返回写出的字节数---成功   -1---失败

前3个参数与write()相同,参数flags是传输控制标志。
(2)recv()函数用于数据的发送。

#include <sys/types.h>
#include < sys/socket.h >         
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 
回:返回读入的字节数---成功   -1---失败

前3个参数与read()相同,参数flags是传输控制标志。

6.关闭套接字
close函数关闭套接字

#include <unistd.h>
int close(int sockfd);

TCP客户端

1.创建套接字
2.连接服务器
TCP用connect函数来建立与TCP服务器的连接。

 #include <sys/socket.h>      
 int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);  
 返回:0---成功   -1---失败

客户端发送的SYN包可能会遇到失败,可能有以下几种情况:
1. 如果客户端没有收到SYN的响应包,根据TCP的超时重发机制进行重发。75秒后还没收到,就返回错误。
2. 如果目的主机没有监听目的端口号,就会返回一个RST的分节,客户端收到RST后立刻返回错误。
3. 如果SYN在中间路由遇到目的不可达,客户端收到ICMP报文,客户端保存这个报文信息,并采用第一种情况方案解决,也就是重发。

3.收发数据
4.关闭套接字

套接字编程流程图

这里写图片描述

TCP回射服务端程序

  1. 服务端程序监听本机所有IP,监听的端口号为8888
/******** 头文件 *********/
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <strings.h>
#include <stdlib.h>
#include <stdio.h>

/******** 宏定义 ********/
#define BUFFSIZE 100

int main()
{
    int listenFd, connectFd;
    socklen_t len;
    int n;
    struct sockaddr_in srcAddr, cliAddr;
    char buf[BUFFSIZE];

    //create socket
    if((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("socket error");
        exit(1);
    }

    bzero(&srcAddr, sizeof(srcAddr));
    srcAddr.sin_family = AF_INET;
    srcAddr.sin_port = htons(8888);
    srcAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    //绑定套接字
    if(bind(listenFd, (struct sockaddr*)&srcAddr, sizeof(srcAddr)) == -1)
    {
        perror("bind error");
        exit(1);
    }

    //监听套接字
    if(listen(listenFd, 5) == -1)
    {
        perror("listen error");
        exit(1);
    }

    len = sizeof(cliAddr);

    printf("listening ...\n");

    while(1)
    {
        //从已完成队列中抓取一个套接字进行服务
        if((connectFd = accept(listenFd, (struct sockaddr*)&cliAddr, &len)) == -1)
        {
            perror("accept error");
            exit(1);
        }

        printf("accepted successful\n");

        //从套接字中读取从客户端发来的数据
        while((n = read(connectFd, buf, BUFFSIZE)) > 0)
        {
            if(buf[n] != '\0')
            {
                buf[n] = '\0';
            }
            printf("receive a message: %s", buf);

            //将读取的数据写进套接字,发给客户端
            if(write(connectFd, buf, n) < 0)
            {
                perror("write error");
                exit(1);
            }
        }
    }

    //进程退出后,所有打开的文件描述符都会被关闭,因此打开的套接字文件也被关闭
    return 0;
}

TCP回射客户端程序

/******** 头文件 *********/
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>

/******** 宏定义 *********/
#define BUFFSIZE 100

int main()
{
    int sockFd;
    int n;
    struct sockaddr_in serverAddr;
    char sendBuf[BUFFSIZE], recvBuf[BUFFSIZE];

    //创建套接字
    if((sockFd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket error");
        exit(1);
    }

    bzero(&serverAddr, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

    //连接套接字
    if(connect(sockFd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0)
    {
        perror("connect error");
        exit(1);
    }

    //从标准输入中读取一行数据
    while(fgets(sendBuf, BUFFSIZE, stdin) != NULL)
    {
        //将数据写入到套接字,发送给服务器
        write(sockFd, sendBuf, strlen(sendBuf));

        //从套接字中读取从服务器发来的数据
        n = read(sockFd, recvBuf, BUFFSIZE);

        //将接受到的数据写到标准输出中
        write(STDIN_FILENO, recvBuf, n);
    }

    return 0;
}

猜你喜欢

转载自blog.csdn.net/shanghairuoxiao/article/details/78536133
今日推荐