1- Introduction

TCP客户端与服务器详解
本文详细介绍了TCP客户端与服务器的设计原理及实现细节,包括通信协议的制定、客户端与服务器的交互流程、错误处理机制等内容,并提供了具体的代码示例。

Please indicate the source: http://blog.youkuaiyun.com/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1

1.1 Introduction

  • When writing programs that communicate across a computer network, one must first invent a protocol, an agreement on how those programs will communicate. Before delving into the design details of a protocol, high-level decisions must be made about which program is expected to initiate communication and when responses are expected.
  • For example, a Web server is thought of as a long-running program (or daemon) that sends network messages only in response to requests coming in from the network. The other side of the protocol is a Web client, such as a browser, which always initiates communication with the server. This organization into client and server is used by most network-aware applications Figure 1.1.
  • Clients normally communicate with one server at a time, we might communicate with many different Web servers over some minutes time period. From the server’s perspective, at any given point in time, it is usual for a server to be communicating with multiple clients. Figure 1.2.
  • The client and server may be thought of as communicating via a network protocol, but multiple layers of network protocols are typically involved. We focus on the TCP/IP protocol suite, also called the Internet protocol suite. For example, Web clients and servers communicate using TCP. TCP in turn uses IP, and IP communicates with a datalink layer of some form. If the client and server are on the same Ethernet, we would have the arrangement shown in Figure 1.3.
  • The client and server need not be attached to the same local area network (LAN) as we show in Figure 1.3. In Figure 1.4, we show the client and server on different LANs, with both LANs connected to a wide area network (WAN) using routers.

1.2 A Simple Daytime Client

  • Figure 1.5 is an implementation of a TCP time-of-day client. This client establishes a TCP connection with a server and the server sends back the current time and date in a human-readable format.
#include <netinet/in.h> // struct sockaddr_in{}
#include <stdio.h>  // printf(), fputs()
#include <sys/socket.h>  // socket(), connect(), struct sockaddr {}
#include <strings.h>  // bzero()
#include <arpa/inet.h>  // htons()
#include <unistd.h>  // read()
#include <stdlib.h>  // exit()
#include <errno.h>

#define MAXLINE 4096

int Socket(int family, int type, int protocol)
{
    int n;
    if((n = socket(family, type, protocol)) < 0)
    {
        perror("socket error\n");
        exit(1);
    }
    return n;
}

void Inet_pton(int family, const char *strptr, void *addrptr)
{
    int n;
    if ((n = inet_pton(family, strptr, addrptr)) <= 0)
    {
        printf("inet_pton error for %s\n", strptr);
        exit(1);
    }
}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if(connect(fd, sa, salen) < 0)
    {
        printf("connect error\n");
        exit(1);
    }
}

void Fputs(const char *ptr, FILE *stream)
{
    if (fputs(ptr, stream) == EOF)
    {
        printf("fputs error\n");
        exit(-1);
    }
}

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;  // <netinet/in.h>

    if (argc != 2)
    {
        printf("usage: a.out <IPaddress>\n");
        exit(1);  // #include <stdlib.h>
    }

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);  // <sys/socket.h>

    bzero(&servaddr, sizeof(servaddr));  // #include <strings.h>
    servaddr.sin_family = AF_INET;
    servaddr.sin_port   = htons(13);  // #include <arpa/inet.h>; 13: daytime server
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  // #include <arpa/inet.h>

    Connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

    while((n = read(sockfd, recvline, MAXLINE)) > 0)  // #include <unistd.h>
    {
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);
    }

    if (n < 0)
    {
        printf("read error\n");
        exit(1);
    }

    exit(0);
}

Include our own header: 1

  • unp.h in Section D.1.

Command-line arguments: 2-3

  • This is the definition of the main function along with the command-line arguments.

Create TCP socket: 10–11

  • The socket function creates an Internet (AF_INET) stream (SOCK_STREAM) socket, which is a name for a TCP socket. The function returns an integer descriptor that we can use to identify the socket in all future function calls (e.g., the calls to connect and read that follow).

Specify server’s IP address and port: 12-16

  • We fill in an Internet socket address structure (a sockaddr_in structure named servaddr) with the server’s IP address and port number. We set the entire structure to 0 using bzero, set the address family to AF_INET, set the port number to 13 (which is the well-known port of the daytime server on any TCP/IP host that supports this service), and set the IP address to the value specified as the first command-line argument (argv[1]).
  • The IP address and port number fields in this structure must be in specific formats: We call htons (“host to network short”) to convert the binary port number, and we call inet_pton (“presentation to numeric”) to convert the ASCII command-line argument into the proper format.

Establish connection with server: 17–18

  • The connect function, when applied to a TCP socket, establishes a TCP connection with the server specified by the socket address structure pointed to by the second argument. We must specify the length of the socket address structure as the third argument to connect.
  • #define SA struct sockaddr; which is a generic socket address structure. Everytime one of the socket functions requires a pointer to a socket address structure, that pointer must be cast to a pointer to a generic socket address structure. This is because the socket functions predate the ANSI C standard, so the void * pointer type was not available when these functions were developed.

Read and display server’s reply: 19–25

  • We read the server’s reply and display the result using the standard I/O fputs function. We must be careful when using TCP because it is a byte-stream protocol with no record boundaries. The server’s reply is normally a 26-byte string of the form
    Mon May 26 20 : 58 : 40 2003\r\n
    where \r is the ASCII carriage return and \n is the ASCII linefeed. With a byte-stream protocol, these 26 bytes can be returned in numerous ways: a single TCP segment containing all 26 bytes of data, in 26 TCP segments each containing 1 byte of data, or any other combination that totals to 26 bytes.
  • Normally, a single segment containing all 26 bytes of data is returned, but with larger data sizes, we cannot assume that the server’s reply will be returned by a single read. Therefore, when reading from a TCP socket, we always need to code the read in a loop and terminate the loop when either read returns 0 (i.e., the other end closed the connection) or a value less than 0 (an error).
  • In this example, the end of the record is being denoted by the server closing the connection. This technique is also used by version 1.0 of the Hypertext Transfer Protocol (HTTP). Other techniques are available. For example, Sun Remote Procedure Call (RPC) and the Domain Name System (DNS) place a binary count containing the record length in front of each record that is sent when using TCP.
  • The important concept is that TCP itself provides no record markers: If an application wants to delineate the ends of records, it must do so itself.

Terminate program: 26

  • exit terminates the program. Unix always closes all open descriptors when a process terminates, so our TCP socket is now closed.

1.3 Protocol Independence

  • Figure 1.6 shows a version that works under IPv6, with the changes highlighted in bold.

#include <netinet/in.h> // struct sockaddr_in{}
#include <stdio.h>  // printf(), fputs()
#include <sys/socket.h>  // socket(), connect(), struct sockaddr {}
#include <strings.h>  // bzero()
#include <arpa/inet.h>  // htons()
#include <unistd.h>  // read()
#include <stdlib.h>  // exit()

#define MAXLINE 4096
#define SA struct sockaddr

int main(int argc, char **argv)
{
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in6 servaddr;  // <netinet/in.h>

    if (argc != 2)
    {
        printf("usage: a.out <IPaddress>\n");
        exit(-1);  // #include <stdlib.h>
    }

    if ((sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0)  // <sys/socket.h>
    {
        printf("socket error\n");
        exit(-1);
    }

    bzero(&servaddr, sizeof(servaddr));  // #include <strings.h>
    servaddr.sin6_family = AF_INET6;
    servaddr.sin6_port   = htons(13);  // #include <arpa/inet.h>; 13: daytime server
    if (inet_pton(AF_INET6, argv[1], &servaddr.sin6_addr) <= 0)  // #include <arpa/inet.h>
    {
        printf("inet_pton error for %s\n", argv[1]);
        exit(-1);
    }

    if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)  // #include <sys/socket.h>
    {
        printf("connect error\n");
        exit(-1);
    }

    while ( (n = read(sockfd, recvline, MAXLINE)) > 0)  // #include <unistd.h>
    {
        recvline[n] = 0;    /* null terminate */
        if (fputs(recvline, stdout) == EOF)
        {
            printf("fputs error\n");
            exit(-1);
        }
    }
    if (n < 0)
    {
        printf("read error\n");
        exit(-1);
    }

    exit(0);
}
  • Figure 11.11 will show a version of this client that is protocol-independent by using the getaddrinfo function which is called by tcp_connect.

1.4 Error Handling: Wrapper Functions

  • We can shorten our programs by defining a wrapper function that performs the actual function call, tests the return value, and terminates on an error. The convention we use is to capitalize the name of the function, as in
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);
    Our wrapper function is shown in Figure 1.7.

1.5 A Simple Daytime Server

#include <sys/socket.h>  // socket()
#include <strings.h>  // bzero()
#include <arpa/inet.h>  // htonl(), htons()
#include <stdlib.h>  // getenv()
#include <time.h>  // time()
#include <stdio.h>
#include <errno.h>  // perror()
#include <unistd.h>  // close()
#include <string.h>  // strlen()

#define MAXLINE 4096
#define LISTENQ 1024

int Socket(int family, int type, int protocol)
{
    int n;
    if((n = socket(family, type, protocol)) < 0)
    {
        perror("socket error\n");
        exit(1);
    }
    return n;
}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if(bind(fd, sa, salen) < 0)
    {
        perror("bind error");
        exit(1);
    }
}

void Listen(int fd, int backlog)
{
    char *ptr;
    if((ptr = getenv("LISTENQ")) != NULL)
    {
        backlog = atoi(ptr);
    }
    if(listen(fd, backlog) < 0)
    {
        perror("listen error\n");
        exit(1);
    }
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
    int n;
again:
    if((n = accept(fd, sa, salenptr)) < 0)
    {
        #ifdef EPROTO
        if(errno == EPROTO || errno == ECONNABORTED)
        #else
        if(errno == ECONNABORTED)
        #endif
        {
            goto again;
        }
        else
        {
            perror("accept error\n");
            exit(1);
        }
    }
    return n;
}

void Write(int fd, void *ptr, size_t nbytes)
{
    if(write(fd, ptr, nbytes) != nbytes)
    {
        perror("write error\n");
        exit(1);
    }
}

void Close(int fd)
{
    if(close(fd) == -1)
    {
        perror("close error\n");
        exit(1);
    }
}

int main(int argc, char **argv)
{
    int listenfd, connfd;
    struct sockaddr_in servaddr;
    char buff[MAXLINE];
    time_t ticks;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);  // <sys/socket.h>

    bzero(&servaddr, sizeof(servaddr));  // <strings.h>
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // <arpa/inet.h>
    servaddr.sin_port = htons(13);  // <arpa/inet.h>, daytime server

    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    for( ; ; )
    {
        connfd = Accept(listenfd, (struct sockaddr *)NULL, NULL);

        ticks = time(NULL);  // <time.h>
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));

        Close(connfd);
    }
}

Create a TCP socket: 10

  • The creation of the TCP socket is identical to the client code.

Bind server’s well-known port to socket: 11–15

  • The server’s well-known port (13 for the daytime service) is bound to the socket by filling in an Internet socket address structure and calling bind. We specify the IP address as INADDR_ANY, which allows the server to accept a client connection on any interface, in case the server host has multiple interfaces.

Convert socket to listening socket: 16

  • By calling listen, the socket is converted into a listening socket, on which incoming connections from clients will be accepted by the kernel. These three steps, socket, bind, and listen, are the normal steps for any TCP server to prepare what we call the listening descriptor (listenfd in this example).
  • #define LISTENQ 1024
    LISTENQ specifies the maximum number of client connections that the kernel will queue for this listening descriptor.

Accept client connection, send reply: 17–21

  • The server process is put to sleep in the call to accept, waiting for a client connection to arrive and be accepted. A TCP connection uses what is called a three-way handshake to establish a connection. When this handshake completes, accept returns, and the return value from the function is a new descriptor (connfd) that is called the connected descriptor. This new descriptor is used for communication with the new client. A new descriptor is returned by accept for each client that connects to our server.
  • The current time and date are returned by the library function time, which returns the number of seconds since the Unix Epoch: 00:00:00 January 1, 1970, Coordinated Universal Time (UTC). The next library function, ctime, converts this integer value into a human-readable string such as
    Mon May 26 20:58:40 2003
  • A carriage return and linefeed are appended to the string by snprintf, and the result is written to the client by write.
  • Calls to sprintf cannot check for overflow of the destination buffer. snprintf requires that the second argument be the size of the destination buffer, and this buffer will not overflow. Other functions that we should be careful with are gets, strcat, and strcpy, normally calling fgets, strncat, and strncpy instead. Even better are the functions strlcat and strlcpy, which ensure the result is a properly terminated string.

Terminate connection: 22

  • The server closes its connection with the client by calling close. This initiates the normal TCP connection termination sequence: a FIN is sent in each direction and each FIN is acknowledged by the other end.

1.6 Roadmap to Client/Server Examples in the Text




1.7 OSI Model(Open Systems Interconnection)

  • Why do sockets provide the interface from the upper three layers of the OSI model into the transport layer?
    1. First, the upper three layers handle all the details of the application and know little about the communication details. The lower four layers know little about the application, but handle all the communication details: sending data, waiting for acknowledgments, sequencing data that arrives out of order, calculating and verifying check-sums, and so on.
    2. The second reason is that the upper three layers often form what is called a user process while the lower four layers are normally provided as part of the operating system (OS) kernel. Unix provides this separation between the user process and the kernel. Therefore, the interface between layers 4 and 5 is the natural place to build the API.

1.8 BSD Networking History

1.9 Test Networks and Hosts

  • The notation “/24” indicates the number of consecutive bits starting from the leftmost bit of the address used to identify the network and subnet.

Discovering Network Topology

  • Two commands can be used to discover details of a network: netstat and ifconfig.
  • netstat -i provides information on the interfaces. -n to print numeric addresses, instead of trying to find names for the networks. This shows us the interfaces and their names. -r shows the routing table. -n to print numeric addresses. This also shows the IP address of the default router.
  • The loopback interface is called lo and the Ethernet is called eth0.
  • Given the interface names, we execute ifconfig to obtain the details for each interface.
  • One way to find the IP address of many hosts on the local network is to ping the broadcast address.

1.10 Unix Standards

1.11 64-Bit Architectures

1.12 Summary

Exercises 1.3

Modify the first argument to socket in Figure 1.5 to be 9999. Compile and run the program. What happens? Find the errno value corresponding to the error that is printed. How can you find more information on this error?

  • What happen:
xiang :~/Gao/Notes/NET/UNP/Codes $ ./15client 127.0.0.2
socket error
: Address family not supported by protocol
  • EAFNOSUPPORT Address family not supported (POSIX.1).
    We can man errno

Exercises 1.4

Modify Figure 1.5 by placing a counter in the while loop, counting the number of times read returns a value greater than 0. Print the value of the counter before terminating. Compile and run your new client.

xiang :~/Gao/Notes/NET/UNP/Codes $ ./15client 127.0.0.2
Thu Jul 28 12:19:42 2016
1
xiang :~/Gao/Notes/NET/UNP/Codes $ ./15client 127.0.0.2
Thu Jul 28 12:19:43 2016
1
xiang :~/Gao/Notes/NET/UNP/Codes $ ./15client 127.0.0.2
Thu Jul 28 12:19:43 2016
1
  • The value printed is always 1.

Exercises 1.5

Modify Figure 1.9 as follows: First, change the port number assigned to the sin_port member from 13 to 9999. Next, change the single call to write into a loop that calls write for each byte of the result string. Compile this modified server and start it running in the background. Next, modify the client from the previous exercise (which prints the counter before terminating), changing the port number assigned to the sin_port member from 13 to 9999. Start this client, specifying the IP address of the host on which the modified server is running as the command-line argument. What value is printed as the client’s counter? If possible, also try to run the client and server on different hosts.

  • We declare an int named i and change the call to write to be the following:
for (i = 0; i < strlen(buff); i++)
    Write(connfd, &buff[i], 1);
  • The results vary, depending on the client host and server host. If the client and server are on the same host, the counter is normally 1, which means even though the server does 26 writes, the data is returned by a single read. But one combination of client and server may produce two packets, and another combination 26 packets. Our discussion of the Nagle algorithm in Section 7.9 explains one reason for this.
  • Remember that different TCPs do different things with the data and our application must be prepared to read the data as a stream of bytes until the end of the data stream is encountered.

Please indicate the source: http://blog.youkuaiyun.com/gaoxiangnumber1
Welcome to my github: https://github.com/gaoxiangnumber1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值