网络编程之TCP Client/Server Example(一)

时间:2022-12-15 11:05:20

前言

  笔者修改了此处的代码,将unp包装起来的错误处理细节展示出来(与UNP实现方式不同),加上作者所使用的头文件等信息。整体与UNP原来的代码稍有区别

1. 修改自UNP的代码

1.1 server

//5_1_1.c
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#define LISTENQ 10
#define SERV_PORT 9877
#define MAXLINE 20
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void str_echo(int sockfd);
ssize_t writen(int fd, const void *vptr, size_t n);


int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
handle_error("socket");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1)
handle_error("bind");
if(listen(listenfd, LISTENQ) == -1)
handle_error("listen");
for ( ; ; )
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) == -1 )
handle_error("accept");
if ( (childpid = fork()) <0)
handle_error("fork");
if(childpid == 0)
{ /* child process */
if(close(listenfd) == -1)
handle_error("close");
str_echo(connfd); /* process the request */
exit(0);
}
else
if(close(connfd) == -1)
handle_error("close"); /* parent closes connected socket */
}
}


void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
if(writen(sockfd, buf, n) == -1)
handle_error("writen");
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
errx(1,"str_echo: read error");
}

ssize_t writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ( (nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return (-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return (n);
}

1.2 client

#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 9877
#define MAXLINE 20
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
ssize_t writen(int fd, const void *vptr, size_t n);
void str_cli(FILE *fp, int sockfd);
ssize_t readline(int fd, void *vptr, size_t maxlen);
static ssize_t my_read(int fd, char *ptr) ;
static int read_cnt;
static char *read_ptr;
static char read_buf[MAXLINE];

int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;

if (argc != 2)
errx(1,"usage: tcpcli <IPaddress>");

if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
handle_error("socket");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) == -1)
handle_error("inet_pton");

if(connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1)
handle_error("connect");
str_cli(stdin, sockfd);
exit(0);
}

void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
int n;
while (fgets(sendline, MAXLINE, fp) != NULL)
{
if(writen(sockfd, sendline, strlen(sendline)) == -1)
handle_error("writen");
if((n=readline(sockfd, recvline, MAXLINE)) == -1)
handle_error("readline");
else if(n == 0)
errx(1,"str_cli: server terminated prematurely");
else
if(fputs(recvline, stdout) == EOF)
errx(1,"error in fputs");
}
errx(1,"fgets:end of file or error");
}


ssize_t writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ( (nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return (-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return (n);
}


static ssize_t my_read(int fd, char *ptr)
{
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return (-1);
} else if (read_cnt == 0)
return (0);
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return (1);
}

ssize_t readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break; /* newline is stored, like fgets() */
} else if (rc == 0) {
*ptr = 0;
return (n - 1); /* EOF, n - 1 bytes were read */
} else
return (-1); /* error, errno set by read() */
}
*ptr = 0; /* null terminate like fgets() */
return (n);
}

  这个readline函数的实现是具有缺陷的,在使用select函数会遇到问题:

System functions like select still won’t know about readline’s internal buffer, so a carelessly written program could easily find itself waiting in select for data already received and stored in readline’s buffers.

2. Normal Startup

  只运行server时,使用netstate观察如下:

[root@localhost ~]# ./5_1_1 &
[1] 1370
[root@localhost ~]# netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN

  运行client时建立连接时,UNP针对该过程有以下解释:

The client calls socket and connect, the latter causing TCP’s three-way handshake to take place. When the three-way handshake completes, connect returns in the client and accept returns in the server. The connection is established. The following steps then take place:

  • The client calls str_cli, which will block in the call to fgets, because we have not typed a line of input yet.
  • When accept returns in the server, it calls fork and the child calls str_echo. This function calls readline, which calls read, which blocks while waiting for a line to be sent from the client.
  • The server parent, on the other hand, calls accept again, and blocks while waiting for the next client connection.

NOTE : When the three-way handshake completes, we purposely list the client step first, and then the server steps. The reason can be seen in Figure 2.5: connect returns when the second segment of the handshake is received by the client, but accept does not return until the third segment of the handshake is received by the server, one-half of the RTT after connect returns.

  以上的陈述表面此时server,server child,client都会出于sleep状态,各自分别阻塞在accept,read,fgets三个函数。为了验证这一结论,输入以下命令:

[root@localhost ~]# ps -ao pid,ppid,tty,stat,args,wchan 
PID PPID TT STAT COMMAND WCHAN
1637 1094 pts/0 S+ ./5_1_1 inet_csk_accept
1638 1432 pts/1 S+ ./5_1_2 127.0.0.1 wait_woken
1639 1637 pts/0 S+ ./5_1_1 wait_woken

  再次运行netstate观察结果如下:

[root@localhost ~]# netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:37394 localhost:9877 ESTABLISHED
tcp 0 0 localhost:9877 localhost:37394 ESTABLISHED

  可以发现在server中bind的通配地址和9877端口,在建立连接之后通配地址变为localhost,端口号不变。第一个tcp描述的server的listen socket,第二个tcp描述client的connected socket,第三个tcp描述server child的connected socket。

3. Normal Termination

  正常发送数据看看结果:

[root@localhost ~]# ./5_1_2 127.0.0.1
hello love*
hello love*
5_1_2: fgets:end of file or error //键入ctrl+D
[root@localhost ~]#

  此时需要立马执行netstate才能观察到以下结果:

[root@localhost ~]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:37396 localhost:9877 TIME_WAIT

  UNP描述这一结束的过程如下:

  • When we type our EOF character, fgets returns a null pointer and the function str_cli (Figure 5.5) returns.
  • When str_cli returns to the client main function (Figure 5.4), the latter terminates by calling errx.
  • Part of process termination is the closing of all open descriptors, so the client socket is closed by the kernel. This sends a FIN to the server, to which the server TCP responds with an ACK. This is the first half of the TCP connection termination sequence. At this point, the server socket is in the CLOSE_WAIT state and the client socket is in the FIN_WAIT_2 state.
  • When the server TCP receives the FIN, the server child is blocked in a call to readline, and readline then returns 0. This causes the str_echo function to return to the server child main.
  • The server child terminates by calling exit.
  • All open descriptors in the server child are closed. The closing of the connected socket by the child causes the final two segments of the TCP connection termination to take place: a FIN from the server to the client, and an ACK from the client. At this point, the connection is completely terminated. The client socket enters the TIME_WAIT state.
  • Finally, the SIGCHLD signal is sent to the parent when the server child terminates. This occurs in this example, but we do not catch the signal in our code,and the default action of the signal is to be ignored. Thus, the child enters the zombie state. We can verify this with the ps command.

4. 最后

  上述过程的描述最好结合这篇,辅助以TCP状态转换图及完整流程图会有更加深刻的理解。