相信你已经基本掌握了最最最简单的基于 TCP 连接的服务器编程技术了。本文算打算完成一个小型服务器,该服务器接受客户端发送过来的数据,然后将数据中的字母转换成大写后再发送给客户端。
1. read 和 write
(1) read
在这里,我们需要将 read 函数再次出来讲讲。
以前我们遇到的 read 的返回值一般不是大于 0 就是小于 0. 大于 0 的返回值表示 read 到的数据字节数,小于 0 表示出错:
- 返回值 > 0: 读取的字节数。
- 返回值 < 0: 出错,同时设置 errno 变量。
如果数据读完了,你还继续 read,就会阻塞。
在网络通信程序中,如果通信链路对端关闭,此时 read 函数就会返回 0. 这个特性实际上我们之前学管道的时候也遇到过,如果一端关闭,则 read 返回 0. 实际上,返回 0 表示读到了文件的结尾,也就是说收到了 EOF 字符。而对端关闭后,操作系统会发送一个 EOF 字符过来。
(2) write
对于 write 函数来说,如果对端关闭了,你还继续写,write 返回 -1,errno 设置为 EPIPE,同时进程收到 SIGPIPE 信号。
2. 大写转换服务器编写思路
服务器程序实际上我们可以直接把之前的写的代码修改一下就行了,大体的框架就仍然是安装一个 socket,bind 套接字地址,转换成被动 socket,然后 accept 连接建立通信链路。
一旦链路建立,就开始我们的业务,即从 socket 中读取数据,转换成大写后再写回 socket.
按照 Linux 的一切皆文件的思想,我们把 socket 描述符当成文件描述符直接 read、write 就行了,相当方便,注意,socket 描述符是既可读也可写的,就好像是以 O_RDWR 打开的一样。
3. 程序清单
serv.c 程序接受链接,将客户端发来的数据转换成大写发送回去。cli.c 程序从标准输入读取数据,然后发送到服务器,接着 cli 程序等待处理结果,接收到结果后,将结果打印在屏幕上。
3.1 serv 服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
// 将数据转换成大写,toupper 函数是 C 标准库提供的,可以直接使用。
void upper(char* buf) {
char* p = buf;
while(*p) {
*p = toupper(*p);
++p;
}
}
int main() {
struct sockaddr_in servaddr, cliaddr;
int sockfd, clientfd, ret, n;
socklen_t cliaddrlen;
char buf[64];
// 1. create sockaddr
puts("1. create sockaddr");
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);
// 2. create socket
puts("2. create socket");
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) ERR_EXIT("socket");
// 3. bind sockaddr
puts("3. bind sockaddr");
ret = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret < 0) ERR_EXIT("bind");
// 4. listen
puts("4. listen");
ret = listen(sockfd, 5);
if (ret < 0) ERR_EXIT("listen");
// 5. accept connect
puts("5. accept connect");
cliaddrlen = sizeof(cliaddr);
clientfd = accept(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (clientfd < 0) ERR_EXIT("accept");
printf("client fd: %d\n", clientfd);
printf("sockaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 6. getsockname, 打印新的 socket 绑定的套接字地址
puts("6. getsockname");
cliaddrlen = sizeof(cliaddr);
ret = getsockname(clientfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (ret < 0) ERR_EXIT("getsockaddr");
printf("sockaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 7. send data
while(1) {
// 从客户端读取数据,如果返回 0 表示对端关闭
n = read(clientfd, buf, 63);
if (n == 0) {
puts("peer closed");
break;
}
buf[n] = 0;
// 将缓冲区中的数据转换成大写。
upper(buf);
// 发送回客户端
write(clientfd, buf, n);
}
close(clientfd);
close(sockfd);
return 0;
}
3.2 cli 客户端程序
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)
int main() {
int sockfd, ret, n;
char buf[64];
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(8080);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) ERR_EXIT("socket");
ret = connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret < 0) ERR_EXIT("connect");
cliaddrlen = sizeof(cliaddr);
ret = getsockname(sockfd, (struct sockaddr*)&cliaddr, &cliaddrlen);
if (ret < 0) ERR_EXIT("getsockaddr");
printf("cliaddr: %s:%d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
while(1) {
// 从标准输入读取数据
scanf("%s", buf);
// 如果收到的第一个字母是 q 就退出循环,关闭连接。
if (buf[0] == 'q') break;
// 将数据发送给服务器
write(sockfd, buf, strlen(buf));
// 从服务器读取处理完的数据
n = read(sockfd, buf, 63);
buf[n] = 0;
// 将结果打印在屏幕上
puts(buf);
}
close(sockfd);
return 0;
}
3.3 编译和运行
$ gcc serv.c -o serv
$ gcc cli.c -o cli
图1 运行结果
可以看到我们的程序可以正常运行啦,最后在客户端输入 q 后,客户端关闭了连接,服务器收到 EOF 后,read 返回了 0,就打印 peer closed(对端关闭),然后退出。
4. 总结
- 掌握基本 TCP 编程步骤