基于TCP/IP的简单的聊天程序

1. 概念

(1) socket API,端口

TCP/IP协议集成到操作系统的内核中,TCP/IP协议中引入了一种称之为"Socket(套接字)"应用程序接口"端口在计算机编程上也就是"Socket接口"。计算机可以通过应用层程序(调用Socket接口)的方式进行通信。


(2) 协议

协议由三要素组成:

[1] 语法,即数据控制信息的结构或格式;

[2] 语义,即需要发出何种控制信息,完成何种动作以及做出何种响应;

[3] 时序(同步),即事件实现顺序的详细说明。

协议总是指某一层的协议。准确地说,它是在同等层之间的实体通信时,有关通信规则和约定的集合就是该层协议。


2. practice

(1) 基于TCP协议的客户端/服务器程序流程

基于TCP协议程序意思:Call Socket API to write an application of TCP protocol. Why it will use ip address(socket地址)?BecauseTCP/IP协议中,“IP地址+TCPUDP端口号”唯一标识网络通讯中的一个进程。


三方握手

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


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


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


ifconfig

The "inet addr" of  lo is used to client's connect function which used to describe server's ip address. From the picture, we can use 127.x.x.x because mask(255.0.0.0).


(2) Interaction code based TCP protocol

server.c

/*Filename:     server.c 
 *Brife:        Be server response client's request based on tcp protocol
 *Author:       One fish
 *Date:         2014.9.8 Mon
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>


#define MAXLINE 80
#define SERV_PORT 8000
#define END_S   "QUIT\n"
#define END_B   "quit\n"


int main(void)
{
        struct sockaddr_in      servaddr, cliaddr;
        socklen_t               cliaddr_len;
        int                     listenfd, connfd;
        char                    buf[MAXLINE] = "hello";
        char                    str[INET_ADDRSTRLEN];
        int                     i, n;

        //Use IPv4 format to be server's address(AF_INET),
        //use tcp protocol to transmit data between server and client(SOCK_STREAM)
        listenfd        = socket(AF_INET, SOCK_STREAM, 0);

        //Set server's ip address and port values
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family             = AF_INET;
        servaddr.sin_addr.s_addr        = htonl(INADDR_ANY);

        //Bind server's network port(socket) with its port and address
        bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
        if (-1 == r_flag) {
                fputs("Bind server ip address and port to socket port failed\n", stderr);
                exit(1);
        }

        //Set the numbers of client  connect to server
        listen(listenfd, 20);

        if (-1 == r_flag) {
                fputs("Set listen number failed\n", stderr);
                exit(1);
        }
        printf("Accepting connections ...\n");
        while (1) {
                cliaddr_len     = sizeof(cliaddr);

                //Return after three times hand-shaking
                connfd          = accept(listenfd, (struct sockaddr *)&cliaddr,
                                        &cliaddr_len);

                printf("received from %s at PORT %d\n",
                        inet_ntop(AF_INET, &cliaddr.sin_addr, str,
                        sizeof(str)),
                        ntohs(cliaddr.sin_port));

                     
                //Response to client
                while ( strcmp(buf,END_S) && strcmp(buf, END_B) ) {
                        memset(buf, '\0', sizeof(buf) );

                        n = read(connfd, buf, MAXLINE);
                        do{
                                printf("From client's infor: %sYour reply(Can't be NULL):", buf);
                        }while( NULL == fgets(rbuf, MAXLINE - 2, stdin) );
     
                        write(connfd,rbuf, strlen(rbuf));
                }
                printf("Close socket\n");
                close(connfd);
        }
}


int socket(int  family, int  type, int  protocol);
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据。IPv4IPv6的地址格式定义在netinet/in.h,IPv4地址用sockaddr_in结构体表示,包括16位端口号和32IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h,sockaddr_un结构体表示。IPv4IPv6UNIX Domain Socket的地址类型分别定义为常数AF_INETAF_INET6AF_UNIX。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。


int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器(socket)需要调用bind绑定一个固定的网络地址和端口号。bind()的作用是将参数sockfdmyaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监myaddr所描述的地址和端口号。



bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(SERV_PORT);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号SERV_PORT,我们定义为8000。网络数据流同样有大端小端之分,网 络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。如果发送主机是小端字节序的,发 送主机把1000填到发送缓冲区之前需要做字节序的转换为大端。可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>


uint32_t htonl(uint32_t  hostlong);

uint16_t htons(uint16_t  hostshort);

uint32_t ntohl(uint32_t  netlong);

uint16_t ntohs(uint16_t   netshort);

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。


int listen(int  sockfd, int  backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。


int accept(int sockfd, struct sockaddr  *cliaddr, socklen_t  *addrlen);

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请,就阻塞等待直到有客户端连接上来。cliaddr是一个传出参数,accept()返回时传出客户端的地址和端口号addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。


client.c

/*Filename:     client.c
 *Brife:        client which will connect to server
 *Author:       one fish
 *Date:         2014.9.8 Mon
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE         80
#define SERV_PORT       8000
#define END_B           "QUIT\n"
#define END_S           "quit\n"


int main(void)
{
        struct sockaddr_in      servaddr;
        char                    buf[MAXLINE];
        int                     sockfd, n, r_flag;
        char                    ibuf[MAXLINE] = "hello";

        sockfd  = -1;
        r_flag  = -1;

        //Use IPv4 to be this client process's address(AF_INET),
        //use tcp protocol to transmit data between server and client(SOCK_STREAM)
        sockfd  = socket(AF_INET, SOCK_STREAM, 0);
        if (-1 == sockfd) {
                fputs("Create socket failed\n", stderr);
                exit(1);
        }

        //Set server's socket[address(127.0.0.1) and port(SERV_PORT)]
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
        servaddr.sin_port = htons(SERV_PORT);

        //Use server's socket to connect to server on this client process
        r_flag  = connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr));
        if (-1 == r_flag) {
                fputs("Connect to server failed\n", stderr);
                exit(1);
        }

        while (strcmp(END_S, ibuf) && strcmp(END_B, ibuf) ) {
                printf("\nInput msg send to server:");
                if ( (NULL == fgets(ibuf, MAXLINE - 2, stdin) )) {
                        continue;
                }

                //Write network port(socket) just as pipe by write() or read()
                write(sockfd, ibuf, strlen(ibuf));
                n = read(sockfd, buf, MAXLINE);

                printf("\nResponse from server:");
               fflush(stdout);
                write(STDOUT_FILENO, buf, n);
        }

        //Close network port(sockfd)
        close(sockfd);
        
        return 0;
}

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


int connect(int sockfd, const struct sockaddr  *servaddr,socklen_t  addrlen);

客户端需要调用connect()连接服务器,connectbind的参数形式一致,区别在于bind的参数是自己的地址,connect的参数是对方的地址。


先编译运行服务器:

lly@debian:~/mydir/lly_books/linux_c_programming_osl/socket$gcc server.c -o server

lly@debian:~/mydir/lly_books/linux_c_programming_osl/socket$./server

Accepting connections ...


编译运行客户端:

root@debian:/home/lly/mydir/lly_books/linux_c_programming_osl/socket#gcc client.c -o client

root@debian:/home/lly/mydir/lly_books/linux_c_programming_osl/socket#./client


The result:


server终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了。


在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为,TCP连接没有完全断开指的是connfd(127.0.0.1:8000)没有完全断开,而我们重新监听的是listenfd(0.0.0.0:8000),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()调用之间插入如下代码:

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

(3) 使用fork并发处理多个client的请求

服务器通常是要同时服务多个客户端,网络服务器通常用fork来同时服务多个客户端,父进程专门负责监听端口,每次accept一个新的客户端连接就fork出一个子进程专门服务这个客户端。但是子进程退出时会产生僵尸进程,父进程要注意处理SIGCHLD信号和调用wait清理僵尸进程。

        while (1) {
                cliaddr_len     = sizeof(cliaddr);

                //Return after three times hand-shaking
                connfd          = accept(listenfd, (struct sockaddr *)&cliaddr,
                                        &cliaddr_len);
                
                //Use one process to manage one client
                r_flag  = fork();
                
                if (-1 == r_flag) {
                        perror("fork");
                        exit(1);
                } else if (0 == r_flag) {       //In child process
                        close(listenfd);        //Save socket only in panrent process
                        
                        pid     = getpid();

                        //Print client's information
                        printf("\nServer-%d received from %s at PORT %d\n",
                                pid,
                                inet_ntop(AF_INET, &cliaddr.sin_addr, str,
                                sizeof(str)),
                                ntohs(cliaddr.sin_port));
                
                        //Response to client
                        while ( strcmp(buf,END_S) && strcmp(buf, END_B) ) {
                                memset(buf, '\0', sizeof(buf) );

                                n = read(connfd, buf, MAXLINE);
     
                                do{
                                        printf("From client's infor: %s\nServer-%d reply(Can't be NULL):", buf, pid);
                                }while( NULL == fgets(rbuf, MAXLINE - 2, stdin) );

                                write(connfd,rbuf, strlen(rbuf));
                        }
                        printf("Close socket, Server-%d exit to be zombies\n", pid);
                        close(connfd);

                        //Child process exit to be zombies
                        exit(1);
                } else {                        //In prant process
                        close(connfd);
                }
        }
To use this frame code will be some problems:


So the code can not  be used code.


[2014.9.8-16:44]

LCNote Over.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值