基于TCP协议的网络程序(基础学习)
下图是基于TCP协议的客户端/服务器程序的一般流程:
图 37.2. TCP协议通讯流程
服务器调用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()则连接处于半关闭状态,仍可接收对方发来的数据。
在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
2.1. 最简单的TCP网络程序 请点评
下面通过最简单的客户端/服务器程序的实例来学习socket API。
server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
/* server.c */ #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 8000int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;listenfd = socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));listen(listenfd, 20);printf("Accepting connections ...\n");while (1) {cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);write(connfd, buf, n);close(connfd);} }
下面介绍程序中用到的socket API,这些函数都在sys/socket.h
中。
int socket(int family, int type, int protocol);
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
while (1) {cliaddr_len = sizeof(cliaddr);connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);n = read(connfd, buf, MAXLINE);...close(connfd); }
client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。
/* client.c */ #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 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;char *str;if (argc != 2) {fputs("usage: ./client message\n", stderr);exit(1);}str = argv[1];sockfd = socket(AF_INET, SOCK_STREAM, 0);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);connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));write(sockfd, str, strlen(str));n = read(sockfd, buf, MAXLINE);printf("Response from server:\n");write(STDOUT_FILENO, buf, n);close(sockfd);return 0; }
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
$ ./serverAccepting connections ...
$ netstat -apn|grep 8000tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8148/server
可以看到server程序监听8000端口,IP地址还没确定下来。现在编译运行客户端:
$ ./client abcd Response from server: ABCD
$ ./serverAccepting connections ...received from 127.0.0.1 at PORT 59757
可见客户端的端口号是自动分配的。现在把客户端所连接的服务器IP改为其它主机的IP,试试两台主机的通讯。
再做一个小实验,在客户端的connect()代码之后插一个while(1);死循环,使客户端和服务器都处于连接中的状态,用netstat命令查看:
$ ./server & [1] 8343 $ Accepting connections ... ./client abcd & [2] 8344 $ netstat -apn|grep 8000 tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 8343/server tcp 0 0 127.0.0.1:44406 127.0.0.1:8000 ESTABLISHED8344/client tcp 0 0 127.0.0.1:8000 127.0.0.1:44406 ESTABLISHED8343/server
应用程序中的一个socket文件描述符对应一个socket pair,也就是源地址:源端口号和目的地址:目的端口号,也对应一个TCP连接。
表 37.1. client和server的socket状态
socket文件描述符 | 源地址:源端口号 | 目的地址:目的端口号 | 状态 |
---|---|---|---|
server.c中的listenfd | 0.0.0.0:8000 | 0.0.0.0:* | LISTEN |
server.c中的connfd | 127.0.0.1:8000 | 127.0.0.1:44406 | ESTABLISHED |
client.c中的sockfd | 127.0.0.1:44406 | 127.0.0.1:8000 | ESTABLISHED |
2.2. 错误处理与读写控制 请点评
上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以迅速得到故障信息。
为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上错误处理代码包装成新的函数,做成一个模块wrap.c:
#include <stdlib.h> #include <errno.h> #include <sys/socket.h>void perr_exit(const char *s) {perror(s);exit(1); }int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {int n;again:if ( (n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))goto again;elseperr_exit("accept error");}return n; }void Bind(int fd, const struct sockaddr *sa, socklen_t salen) {if (bind(fd, sa, salen) < 0)perr_exit("bind error"); }void Connect(int fd, const struct sockaddr *sa, socklen_t salen) {if (connect(fd, sa, salen) < 0)perr_exit("connect error"); }void Listen(int fd, int backlog) {if (listen(fd, backlog) < 0)perr_exit("listen error"); }int Socket(int family, int type, int protocol) {int n;if ( (n = socket(family, type, protocol)) < 0)perr_exit("socket error");return n; }ssize_t Read(int fd, void *ptr, size_t nbytes) {ssize_t n;again:if ( (n = read(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n; }ssize_t Write(int fd, const void *ptr, size_t nbytes) {ssize_t n;again:if ( (n = write(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n; }void Close(int fd) {if (close(fd) == -1)perr_exit("close error"); }
ssize_t Readn(int fd, void *vptr, size_t n) {size_t nleft;ssize_t nread;char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0;elsereturn -1;} else if (nread == 0)break;nleft -= nread;ptr += nread;}return n - nleft; }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;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n; }
static ssize_t my_read(int fd, char *ptr) {static int read_cnt;static char *read_ptr;static char read_buf[100];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;} else if (rc == 0) {*ptr = 0;return n - 1;} elsereturn -1;}*ptr = 0;return n; }
2.3. 把client改为交互式输入 请点评
目前实现的client每次运行只能从命令行读取一个字符串发给服务器,再从服务器收回来,现在我们把它改成交互式的,不断从终端接受用户输入并和server交互。
/* client.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char *argv[]) {struct sockaddr_in servaddr;char buf[MAXLINE];int sockfd, n;sockfd = Socket(AF_INET, SOCK_STREAM, 0);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);Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));while (fgets(buf, MAXLINE, stdin) != NULL) {Write(sockfd, buf, strlen(buf));n = Read(sockfd, buf, MAXLINE);if (n == 0)printf("the other side has been closed.\n");elseWrite(STDOUT_FILENO, buf, n);}Close(sockfd);return 0; }
编译并运行server和client,看看是否达到了你预想的结果。
$ ./client haha1 HAHA1 haha2 the other side has been closed. haha3 $
另外,我们需要修改server,使它可以多次处理同一客户端的请求。
/* server.c */ #include <stdio.h> #include <string.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(void) {struct sockaddr_in servaddr, cliaddr;socklen_t cliaddr_len;int listenfd, connfd;char buf[MAXLINE];char str[INET_ADDRSTRLEN];int i, n;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, 20);printf("Accepting connections ...\n");while (1) {cliaddr_len = sizeof(cliaddr);connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);while (1) {n = Read(connfd, buf, MAXLINE);if (n == 0) {printf("the other side has been closed.\n");break;}printf("received from %s at PORT %d\n",inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),ntohs(cliaddr.sin_port));for (i = 0; i < n; i++)buf[i] = toupper(buf[i]);Write(connfd, buf, n);}Close(connfd);} }
2.4. 使用fork并发处理多个client的请求 请点评
listenfd = socket(...); bind(listenfd, ...); listen(listenfd, ...); while (1) {connfd = accept(listenfd, ...);n = fork();if (n == -1) {perror("call to fork");exit(1);} else if (n == 0) {close(listenfd);while (1) {read(connfd, ...);...write(connfd, ...);}close(connfd);exit(0);} elseclose(connfd); }
2.5. setsockopt 请点评
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:
$ ./serverbind error: Address already in use
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:
$ netstat -apn |grep 8000tcp 1 0 127.0.0.1:33498 127.0.0.1:8000 CLOSE_WAIT 10830/client tcp 0 0 127.0.0.1:8000 127.0.0.1:33498 FIN_WAIT2 -
$ netstat -apn |grep 8000tcp 0 0 127.0.0.1:8000 127.0.0.1:44685 TIME_WAIT -$ ./serverbind error: Address already in use
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2.6. 使用select 请点评
select是网络程序中很常用的一个系统调用,它可以同时监听多个阻塞的文件描述符(例如多个网络连接),哪个有数据到达就处理哪个,这样,不需要fork和多进程就可以实现并发服务的server。
/* server.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include "wrap.h"#define MAXLINE 80 #define SERV_PORT 8000int main(int argc, char **argv) {int i, maxi, maxfd, listenfd, connfd, sockfd;int nready, client[FD_SETSIZE];ssize_t n;fd_set rset, allset;char buf[MAXLINE];char str[INET_ADDRSTRLEN];socklen_t cliaddr_len;struct sockaddr_in cliaddr, servaddr;listenfd = Socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(SERV_PORT);Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));Listen(listenfd, 20);maxfd = listenfd; /* initialize */maxi = -1; /* index into client[] array */for (i = 0; i < FD_SETSIZE; i++)client[i] = -1; /* -1 indicates available entry */FD_ZERO(&allset);FD_SET(listenfd, &allset);for ( ; ; ) {rset = allset; /* structure assignment */nready = select(maxfd+1, &rset, NULL, NULL, NULL);if (nready < 0)perr_exit("select error");if (FD_ISSET(listenfd, &rset)) { /* new client connection */cliaddr_len = sizeof(cliaddr);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));for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {client[i] = connfd; /* save descriptor */break;}if (i == FD_SETSIZE) {fputs("too many clients\n", stderr);exit(1);}FD_SET(connfd, &allset); /* add new descriptor to set */if (connfd > maxfd)maxfd = connfd; /* for select */if (i > maxi)maxi = i; /* max index in client[] array */if (--nready == 0)continue; /* no more readable descriptors */}for (i = 0; i <= maxi; i++) { /* check all clients for data */if ( (sockfd = client[i]) < 0)continue;if (FD_ISSET(sockfd, &rset)) {if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {/* connection closed by client */Close(sockfd);FD_CLR(sockfd, &allset);client[i] = -1;} else {int j;for (j = 0; j < n; j++)buf[j] = toupper(buf[j]);Write(sockfd, buf, n);}if (--nready == 0)break; /* no more readable descriptors */}}} }
基于TCP协议的网络程序(基础学习)相关推荐
- 计算机网络课程设计--基于TCP协议网上聊天程序--python实现带图形界面--socket--多线程
基于TCP协议网上聊天程序 引言 21世纪是一个以网络为核心的信息时代,要实现信息化,就必须依靠完善的网络.而随着计 ...
- 基于TCP协议的网络摄像头的设计与实现
一.摘要 基于TCP协议的网络摄像头的设计大部分和博文"基于UDP协议的网络摄像头的设计与实现"相同,本篇博文采用的TCP协议栈为NicheStack协议栈(同理,可使用LWIP协 ...
- 用C#实现基于TCP协议的网络通讯
TCP协议是一个基本的网络协议,基本上所有的网络服务都是基于TCP协议的,如HTTP,FTP等等,所以要了解网络编程就必须了解基于TCP协议的编程.然而TCP协议是一个庞杂的体系,要彻底的弄清楚它的实 ...
- 用C#实现基于TCP协议的网络通讯(2)
2008-09-09 19:36 作者: 出处:www.4oa.com ( 11 ) 砖 ( 12 ) 好 评论 ( 0 ) 条 进入论坛 更新时间:2005-09-29 14:54 关 键 词 ...
- 用C#实现基于TCP协议的网络通讯(1)
2008-09-09 19:35 作者: 出处:www.4oa.com ( 11 ) 砖 ( 12 ) 好 评论 ( 0 ) 条 进入论坛 更新时间:2005-09-29 14:54 关 键 词 ...
- 基于TCP协议网上聊天程序(python)带界面
基于TCP协议网上聊天程序(python)带界面 1 设计目标 基于TCP协议网上聊天程序 实现一简单的聊天程序实现网上聊天,包括服务器和客户端. (1)支持多人聊天: (2)客户端具有图形化用户界面 ...
- 基于TCP协议的网络聊天室
创建服务端程序 public class Server {public static void main(String[] args) {new Thread(new ServerTask()).st ...
- 篡改计算机网络,基于TCP协议的网络数据实时篡改.doc
基于TCP协议的网络数据实时篡改 基于 TCP 协议的网络数据实时篡改 金 瓯,施 勇,薛 质(上海交通大学信息安全工程学院,上海 200240)[摘 要]首先对网络数据实时篡改作了定义,指出攻击者介 ...
- 浅析C#基于TCP协议的SCOKET通信
TCP协议是一个基本的网络协议,基本上所有的网络服务都是基于TCP协议的,如HTTP,FTP等等,所以要了解网络编程就必须了解基于TCP协议的编程.然而TCP协议是一个庞杂的体系,要彻底的弄清楚它的实 ...
最新文章
- OpenCV(项目)车牌识别3 -- 模板匹配
- 从概念到应用,终于有人把数据挖掘讲明白了
- ASP.NET弹出一个对话框
- 4.1 df命令 4.2 du命令 4.3/4.4 磁盘分区
- 腾讯与 TTN 宣布战略合作,共同推进全球及中国物联网开发生态
- 怎么修改antd mobile中picker样式_修改 iPhone 双信号方法,超级好玩
- Controller向View传值方式总结
- 自动驾驶 | 清华车辆与运载学院最新科研成果公布!
- 【BZOJ2768】冠军调查,网络流之最小割
- cc2530协调器向终端发信息
- 超简单的Spring入门案例制作,快来看看吧!
- 徐思201771010132《面向对象程序设计(java)》第八周学习总结
- 使用 openssl 进行 base64 编解码
- 苹果如何不显示云服务器照片,iPhone12如何隐藏照片 iPhone12不显示照片的三种方法...
- delphi fastreport4.5 的使用
- 宏基(Acer)笔记本(5583)拆机清洗风扇
- matlab把华氏度,MATLAB GUI实例1——摄氏度与华氏度的转换
- 上海大学生计算机一级考试时间,2019上海各大学期末考试时间安排 什么时候期末考试...
- 体育IP价值大爆发 本土赛事IP蕴含着巨大发展潜力
- 超详细EVE-NG安装教程,问题解决,关联CRT和Wireshark(适合新手,内含下载地址)