linux网络程序设计——2 网络模型

本文深入探讨了TCP客户端服务器模型及其在socket API中的应用。详细解释了客户端如何与服务器建立连接、数据传输过程及socket函数的使用。通过实例代码展示了客户端和服务器端的实现,帮助理解TCP/IP协议在实际编程中的应用。

2 C/S模型-TCP

大部分进程间通信使用客户端-服务器模型。进程间通信指的是两个进程之间相互通信,其中,客户端进程连接服务器进程,通常是发出数据请求。一个很好的比喻是一个人给另一个人打电话,打出电话的人就好比客户端,接电话的人就好比服务器。

有两点需要注意,第一,客户端需要知道服务器是否存在,如果服务器存在,服务器的地址是多少;但是在客户端连接服务器之前,服务器并不需要知道客户端的地址(甚至客户端存在与否)。第二,连接一旦建立,客户端和服务器之间就可以接收和发送数据。

客户端和服务器用于建立连接的系统调用有所不同,但都包含socket的基本构建。

客户端建立socket的步骤:

- 使用socket()函数(系统调用)创建一个socket

- 使用connect()函数将socket连接到服务器的地址

- 发送或接收数据。有很多种方法进行发动数据和接收数据,但是最简单的方法是使用read()和write()函数

服务器端建立socket的步骤:

- 使用socket()函数创建一个socket

- 使用bind()函数将socket和一个地址绑定,对于网络上的服务器来讲,地址包含主机和端口号,像这样:127.0.0.1:8888

- 使用listen()函数监听连接

- 当监听到有连接时,使用accept()函数接受一个连接。这个函数通常会堵塞,直到客户端连接建立。

- 接收和发送数据

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

  数据传输的过程:

  1) 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

  如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

实例<客户端/服务器>

mkdir server_test

touch server.c

touch client.c

touch Makefile

 

server.c

#include <sys/types.h>     

#include <sys/socket.h>

#include <arpa/inet.h>

#include <stdio.h>

#include <string.h>

#include <sys/types.h>

#include <netinet/in.h>

 

#define SERVER_PORT 8000

#define MAXLINE 4096

 

int main(void)

{

    struct sockaddr_in serveraddr, clientaddr;

    int sockfd, addrlen, confd, len, i;

    char ipstr[128];

    char buf[MAXLINE];

 

    //1.socket

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.bind

    bzero(&serveraddr, sizeof(serveraddr));

    /* 地址族协议IPv4 */

    serveraddr.sin_family = AF_INET;

    /* IP地址 */

    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    serveraddr.sin_port = htons(SERVER_PORT);

    bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    //3.listen

    listen(sockfd, 128);

    while (1) {

        //4.accept阻塞监听客户端链接请求

        addrlen = sizeof(clientaddr);

        confd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen);//返回的是客户端和服务端专用通道的socket描述符

        //输出客户端IP地址和端口号

        inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr));

        printf("client ip %s\tport %d\n",

                inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, ipstr, sizeof(ipstr)),

                ntohs(clientaddr.sin_port));

 

        //和客户端交互数据操作confd

        //5.处理客户端请求

        len = read(confd, buf, sizeof(buf));

        i = 0;

        while (i < len) {

            buf[i] = toupper(buf[i]);

            i++;

        }

        write(confd, buf, len);

 

        close(confd);

    }

    close(sockfd);

 

    return 0;

}

 

client.c

#include <netinet/in.h>

#include <stdio.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>

#include <string.h>

#include <stdlib.h>

#include <sys/stat.h>

#include <unistd.h>

#include <fcntl.h>

#define SERVER_PORT 8000

#define MAXLINE 4096

int main(int argc, char *argv[])

{

    struct sockaddr_in serveraddr;

    int confd, len;

    char ipstr[] = "127.0.0.1";

    char buf[MAXLINE];

    if (argc < 2) {

        printf("./client str\n");

        exit(1);

    }

    //1.创建一个socket

    confd = socket(AF_INET, SOCK_STREAM, 0);

    //2.初始化服务器地址

    bzero(&serveraddr, sizeof(serveraddr));

    serveraddr.sin_family = AF_INET;

    //"192.168.6.254"

    inet_pton(AF_INET, ipstr, &serveraddr.sin_addr.s_addr);

    serveraddr.sin_port  = htons(SERVER_PORT);

    //3.链接服务器

    connect(confd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

 

    //4.请求服务器处理数据

    write(confd, argv[1], strlen(argv[1]));

    len = read(confd, buf, sizeof(buf));

    write(STDOUT_FILENO, buf, len);

 

    //5.关闭socket

    close(confd);

    return 0;

}

 

Makefile

all:server client

 

server:server.c

    gcc $< -o $@

 

client:client.c

    gcc $< -o $@

 

.PHONY:clean

clean:

    rm -f server

    rm -f client

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。客户端和服务器启动后可以查看链接情况:netstat-apn|grep 8000

3 C/S模型-UDP

实例<客户端/服务器>

udp_server.cpp

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

#include <assert.h>

#include <stdio.h>

#include <string.h>

 

using namespace std;

 

#define BUFFER_SIZE 1024

 

void tsocket(int argc, const char * argv[]);

 

int main(int argc, const char * argv[]) {

    tsocket(argc,argv);

    return 0;

}

void tsocket(int argc, const char * argv[]){

    if(argc < 3){

        exit(-1);

    }

 

    const char* ip = argv[1];

    int port = atoi(argv[2]);

    int backlog = atoi(argv[3]);

 

    std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog  << std::endl;

 

    int fd;

    int check_ret;

 

    fd = socket(PF_INET,SOCK_DGRAM , 0);

    assert(fd >= 0);

 

    struct sockaddr_in address;

    bzero(&address,sizeof(address));

 

    //转换成网络地址

    address.sin_port = htons(port);

    address.sin_family = AF_INET;

    //地址转换

    inet_pton(AF_INET, ip, &address.sin_addr);

 

    //绑定ip和端口

    check_ret = bind(fd,(struct sockaddr*)&address,sizeof(address));

    assert(check_ret >= 0);

 

    while(1){

        char buffer[BUFFER_SIZE];

        struct sockaddr_in addressClient;

        socklen_t clientLen = sizeof(addressClient);

        memset(buffer, '\0', BUFFER_SIZE);

        //获取信息

        if(recvfrom(fd, buffer, BUFFER_SIZE-1,0,(struct sockaddr*)&addressClient, &clientLen) == -1)

        {

           perror("Receive Data Failed:");

           exit(1);

        }

        printf("buffer=%s\n", buffer);

    }

    close(fd);

}

 

udp_client.cpp

#include <iostream>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <signal.h>

#include <unistd.h>

#include <stdlib.h>

#include <assert.h>

#include <stdio.h>

#include <string.h>

 

using namespace std;

 

void tserver(int argc, const char * argv[]);

 

int main(int argc, const char * argv[]) {

    tserver(argc,argv);

    return 0;

}

void tserver(int argc, const char * argv[]){

    std::cout << "t server" << std::endl;

    if(argc < 3){

        exit(-1);

    }

 

    const char* ip = argv[1];

    int port = atoi(argv[2]);

    int backlog = atoi(argv[3]);

 

    std::cout << "ip=" << ip << " port="<<port << " backlog=" << backlog  << std::endl;

 

    int fd;

    int check_ret;

 

    fd = socket(PF_INET,SOCK_DGRAM , 0);

    assert(fd >= 0);

 

    struct sockaddr_in addressServer;

    bzero(&addressServer,sizeof(addressServer));

 

    //转换成网络地址

    addressServer.sin_port = htons(port);

    addressServer.sin_family = AF_INET;

    //地址转换

    inet_pton(AF_INET, ip, &addressServer.sin_addr);

    //发送数据

    const char* normal_data = "my boy!";

    if(sendto(fd, normal_data, strlen(normal_data),0,(struct sockaddr*)&addressServer,sizeof(addressServer)) < 0)

    {

      perror("Send File Name Failed:");

      exit(1);

    }

    close(fd);

}

4 select模型

select系统调用时用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。

文件句柄,其实就是一个整数,表示一个系统资源,通过socket函数的声明就明白了:

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

我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE*结构的表示就是stdin、stdout、stderr。

继续上面的select,就是用来监视某个或某些句柄的状态变化的。select函数原型如下:

int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

函数的最后一个参数timeout是一个超时时间值。其类型是struct timeval *,即一个struct timeval结构的变量的指针,所以我们在程序里要声明一个struct timeval tv;然后把变量tv的地址&tv传递给select函数。struct timeval结构如下:

struct timeval

{

       long tv_sec;  //seconds

       long tv_usec; //microseconds

};

第2、3、4三个参数是一样的类型fd_set *,即我们在程序里要申请几个fd_set类型的变量,比如rdfds,wtfds,exfds,然后把这个变量的地址&rdfds,&wtfds,&exfds传递给select函数。这三个参数都是一个句柄的集合,第一个rdfds是用来保存这样的句柄的:当句柄的状态变成可读时系统就告诉select函数返回,同理第二个函数是指向有句柄状态变成可写时系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即句柄上有特殊情况发生时系统会告诉select函数返回。特殊情况比如对方通过一个socket句柄发来了紧急数据。如果我们程序里只想检测某个socket是否有数据可读,我们可以这样:

fd_set  rdfds;

struct timeval tv;

int ret;

FD_ZERO(&rdfds);

FD_SET(socket, &rdfds);

tv.tv_sec = 1;

tv.tv_uses = 500;

ret = select (socket + 1, &rdfds, NULL, NULL, &tv);

if(ret < 0) perror (“select”);

else if (ret==0) printf(“time out”);

else {

    printf(“ret = %d/n”,ret);

    if(FD_ISSET(socket, &rdfds)){

        /* 读取socket句柄里的数据 */

        recv(...);

    }

}

注意select函数的第一个参数,是所有加入集合的句柄值的最大那个那个值还要加1.比如我们创建了3个句柄;

int sa, sb, sc;

sa = socket(……);

connect (sa,….);

 

sb = socket(….);

connect (sb,…);

 

sc = socket(….);

connect(sc,…);

 

FD_SET(sa, &rdfds);

FD_SET(sb, &rdfds);

FD_SET(sc, &rdfds);

在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下:

int maxfd = 0;

if(sa > maxfd) maxfd = sa;

if(sb > maxfd) maxfd = sb;

if(sc > maxfd) maxfd = sc;

然后调用select函数:

ret = select (maxfd+1, &rdfds, NULL, NULL,&tv);

同样的道理,如果我们是检测用户是否按了键盘进行输入,我们就应该把标准输入0这个句柄放到select里来检测,如下:

FD_ZERO(&rdfds);

FD_SET(0, &rdfds);

tv.tv_sec = 1;

tv.tv_usec = 0;

ret = select (1, &rdfds,NULL,NULL,&tv);

if(ret < 0) perror(“select”);

else if (ret = = 0) printf (“time out/n”);

else{

    scanf(“%s”,buf);

}

 实例<服务器>

使用select函数可以以非阻塞的方式和多个socket通信。程序只是演示select函数的使用,功能非常简单,即使某个连接关闭以后也不会修改当前连接数,连接数达到最大值后会终止程序。

1. 程序使用了一个数组fd_A,通信开始后把需要通信的多个socket描述符都放入此数组。

2. 首先生成一个叫sock_fd的socket描述符,用于监听端口。

3. 将sock_fd和数组fd_A中不为0的描述符放入select将检查的集合fdsr。

4. 处理fdsr中可以接收数据的连接。如果是sock_fd,表明有新连接加入,将新加入连接的socket描述符放置到fd_A。

select_server.cpp

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h>

#include <string.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

 

#define MYPORT 1234    // the port users will be connecting to

 

#define BACKLOG 5     // how many pending connections queue will hold

 

#define BUF_SIZE 200

 

int fd_A[BACKLOG];    // accepted connection fd

int conn_amount;    // current connection amount

 

void showclient()

{

    int i;

    printf("client amount: %d\n", conn_amount);

    for (i = 0; i < BACKLOG; i++) {

        printf("[%d]:%d  ", i, fd_A[i]);

    }

    printf("\n\n");

}

 

int main(void)

{

    int sock_fd, new_fd;  // listen on sock_fd, new connection on new_fd

    struct sockaddr_in server_addr;    // server address information

    struct sockaddr_in client_addr; // connector's address information

    socklen_t sin_size;

    int yes = 1;

    char buf[BUF_SIZE];

    int ret;

    int i;

 

    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

        perror("socket");

        exit(1);

    }

 

    if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {

        perror("setsockopt");

        exit(1);

    }

   

    server_addr.sin_family = AF_INET;         // host byte order

    server_addr.sin_port = htons(MYPORT);     // short, network byte order

    server_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP

    memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));

 

    if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {

        perror("bind");

        exit(1);

    }

 

    if (listen(sock_fd, BACKLOG) == -1) {

        perror("listen");

        exit(1);

    }

 

    printf("listen port %d\n", MYPORT);

 

    fd_set fdsr;

    int maxsock;

    struct timeval tv;

 

    conn_amount = 0;

    sin_size = sizeof(client_addr);

    maxsock = sock_fd;

    while (1) {

        // initialize file descriptor set

        FD_ZERO(&fdsr);

        FD_SET(sock_fd, &fdsr);

 

        // timeout setting

        tv.tv_sec = 30;

        tv.tv_usec = 0;

 

        // add active connection to fd set

        for (i = 0; i < BACKLOG; i++) {

            if (fd_A[i] != 0) {

                FD_SET(fd_A[i], &fdsr);

            }

        }

 

        ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);

        if (ret < 0) {

            perror("select");

            break;

        } else if (ret == 0) {

            printf("timeout\n");

            continue;

        }

 

        // check every fd in the set

        for (i = 0; i < conn_amount; i++) {

            if (FD_ISSET(fd_A[i], &fdsr)) {

                ret = recv(fd_A[i], buf, sizeof(buf), 0);

                if (ret <= 0) {        // client close

                    printf("client[%d] close\n", i);

                    close(fd_A[i]);

                    FD_CLR(fd_A[i], &fdsr);

                    fd_A[i] = 0;

                } else {        // receive data

                    if (ret < BUF_SIZE)

                        memset(&buf[ret], '\0', 1);

                    printf("client[%d] send:%s\n", i, buf);

                }

            }

        }

 

        // check whether a new connection comes

        if (FD_ISSET(sock_fd, &fdsr)) {

            new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);

            if (new_fd <= 0) {

                perror("accept");

                continue;

            }

 

            // add to fd queue

            if (conn_amount < BACKLOG) {

                fd_A[conn_amount++] = new_fd;

                printf("new connection client[%d] %s:%d\n", conn_amount,

                        inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

                if (new_fd > maxsock)

                    maxsock = new_fd;

            }

            else {

                printf("max connections arrive, exit\n");

                send(new_fd, "bye", 4, 0);

                close(new_fd);

                break;

            }

        }

        showclient();

    }

 

    // close other connections

    for (i = 0; i < BACKLOG; i++) {

        if (fd_A[i] != 0) {

            close(fd_A[i]);

        }

    }

 

    exit(0);

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值