基于C语言的TCP socket 网络编程

2014-07-06 09:28:47   最后更新: 2014-07-06 09:34:19   访问数量:1546




本文分别实现了最简单的基于C语言的TCP socket服务器与客户端程序,出于学习的目的,本文中的程序只是 unix 提供的系统调用进行了简单的封装,并进行了相应的错误处理,读者可以在此基础上对所用到的系统调用进行进行的封装,使主程序更加简洁

关于本文中的各种用法,可以参看TCP连接的建立和终止、基本TCP套接字函数、套接字编程的常用函数和迭代服务器与并发服务器做进一步的了解

 

本文的服务器实现只是一个简单的回射并发服务器,即将收到的文本再发送回去,对于更复杂的操作,读者可以自行改写处理函数

服务器端代码如下:

/* * file: main.c * author: 龙泉居士 * date: 2013-06-03 15:47 */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/socket.h> #include "function/function.h" #define SERV_PORT 8000 #define LISTENQ 1024 int main () { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; if ( (listenfd = socket (AF_INET, SOCK_STREAM, 0)) <= 0) { perror ("socket error: "); return -1; } 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))) { perror ("bind error: "); return -1; } if (listen(listenfd, LISTENQ)) { perror ("listen error: "); return -1; } while (1) { clilen = sizeof (cliaddr); if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) <= 0) { perror ("accept error: "); return -1; } if ((childpid = fork()) < 0) { perror ("fork error: "); return -1; } if (childpid == 0) { /* child process */ if (close (listenfd)) { perror ("close listenfd error"); return -1; } str_echo (connfd); return 0; } if (close (connfd)) { perror ("close connfd error: "); return -1; } } }

 

/* * file: function/function.h * author: 龙泉居士 * date: 2013-06-03 15:48 */ #ifndef _FUNCTION_H_20130603_ #define _FUNCTION_H_20130603_ void str_echo (int); ssize_t writen (int, const void *, size_t); #endif

 

/* * file: function/function.c * author: 龙泉居士 * date: 2013-06-03 15:48 */ #include <stdio.h> #include <unistd.h> #include <errno.h> #include "function.h" #define MAXLINE 1024 void str_echo (int sockfd) { ssize_t n; char buf[MAXLINE]; again_read: while ((n = read (sockfd, buf, MAXLINE)) > 0) if (writen (sockfd, buf, n) < 0) return; if (n < 0 && errno == EINTR) goto again_read; else if (n < 0) { perror ("read error: "); return; } } 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; else { perror ("write error: "); return -1; } } nleft -= nwritten; ptr += nwritten; } return n; }

 

客户端程序从标准输入读入数据并发送给服务器端,然后将接下来接收到的文本打印出来

代码如下:

/* * file: main.c * author: 龙泉居士 * date: 2013-06-03 15:54 */ #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "function/function.h" #define SERV_PORT 8000 int main (int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) { printf ("usage: tcpcli <IPaddress>"); return -1; } if ((sockfd = socket (AF_INET, SOCK_STREAM, 0)) <= 0) { perror ("socket error: "); return -1; } 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) < 0) { perror ("inet_pton error: "); return -1; } if (connect (sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { perror ("connect error: "); return -1; } str_cli (stdin, sockfd); return 0; }

 

/* * file: function/function.h * author: 龙泉居士 * date: 2013-06-03 15:55 */ #ifndef _CLIENT_FUNCTION_H_20130603_ #define _CLIENT_FUNCTION_H_20130603_ void str_cli (FILE *, int); ssize_t writen (int, const void *, size_t); ssize_t readline (int, void *, size_t); #endif

 

/* * file: function/function.c * author: 龙泉居士 * date: 2013-06-03 15:55 */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include "function.h" #define MAXLINE 1024 void str_cli (FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (fgets (sendline, MAXLINE, fp) != NULL) { if (writen (sockfd, sendline, strlen(sendline)) < 0) return; if (readline (sockfd, recvline, MAXLINE) == 0) { printf ("str_cli: server terminated prematurely"); return; } fputs (recvline, stdout); } } 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; else { perror ("write error: "); return -1; } } nleft -= nwritten; ptr += nwritten; } return n; } 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++) { again: if ((rc = read(fd, &c, 1)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { *ptr = 0; return n-1; } else { if (errno == EINTR) goto again; return -1; } } *ptr = 0; return n; }

 

服务器启动后,依次调用 socket、bind、listen 和 accept 以完成必要的初始化工作,并阻塞于 accept 调用,在启动客户之前,我们可以运行 netstat 程序来检查服务器监听套接字的状态。

使用 netstat -a 命令可以查看监听套接字,如下图所示

 

由于该命令会有大量输出,所以我们选择将结果重定向到一个文件中,在文件中我们可以看到这个命令的结果:

 

可以看到第4行就是我们关心的监听套接字,本地端口为80000,*:*表示一个为0的IP地址(即 INADDR_ANY,通配地址)或为0的端口号

 

启动客户端程序后,客户首先调用 socket 和 connect,分别创建套接字和引起TCP 的三路握手,当三路握手完成后,客户中的 connect 和 服务器中的 accept 均返回(connect在客户接收到三路握手的第二个分节时返回,accept 在服务器接受到三路握手的第三个分节时返回),连接于是建立,开始客户与服务器间的流通信过程

 

我们在一台主机上运行客户和服务器,再调用 netstat -a 命令可以得到下面的结果:

 

结果中出现了两个新的 ESTABLISHED 状态的套接字,并且有两个端口号为 8000 的套接字

第6行对应于服务器子进程的套接字,因为他的本地端口号是8000,且只监听客户的58349端口,而第4行对应于服务器父进程的套接字,他仍然监听IP地址0(INADDR_ANY)

 

我们也可以使用ps命令来检查进程状态和关系,如下图所示:

 

我们使用这些参数指定只显示我们关心的内容

我们根据 PID 与 PPID 可以看出哪个进程是哪个进程的子进程

我们的三个网络进程都是S+状态,表示进程正在为等待某些资源而睡眠,进程处于睡眠状态时,WCHAN 列出相应的条件

 

当连接建立后,无论在客户端输入什么,服务器端都会将收到的数据返回到客户端,并显示到标准输出

当我们在客户端中键入EOF字符(Ctrl-D)以终止客户端后,再次使用 netstat 命令,将得到如下结果:

 

正常终止客户和服务器的步骤如下(可以参看:TCP连接的建立和终止):

  1. 客户端main通过调用exit终止
  2. 进程终止处理的部分工作是关闭所有打开的描述符,因此客户打开的套接字由内核关闭,导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,至此,服务器套接字处于CPOSE_WAIT状态,客户套接字则处于FIN_WAIT_2状态
  3. 当服务器TCP接收FIN时,服务器子进程阻塞于readline调用,返回0,接着,返回服务器子进程main函数,调用exit终止
  4. 服务器子进程中打开的所有描述符中随之关闭,由子进程来关闭已连接套接字会引发TCP连接终止序列的后两个分节:一个从服务器到客户的FIN和一个从客户到服务器的ACK,至此,连接完全终止,客户套接字进入 TIME_WAIT 状态

进程终止处理的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号,我们在程序中并没有捕获这个信号,从而将该信号进行了忽略,未加处理,子进程于是进入僵死状态,通过 ps 命令可以验证这一点,如下图所示:

 

我们看到,子进程的状态为Z,即僵死状态

 






linux      unix      c++      cpp      c语言      unp      unix网络编程      tcp      网络编程      socket      龙潭书斋      套接字     


京ICP备15018585号