TCP客户/服务器模型
struct sockaddr是一个通用地址,如果用ipv4,需要将ipv4的地址结构struct sockaddr_in强制转换为通用的地址结构
套接字一旦传递给listen,就变成了被动套接字。主动套接字会调用connect()函数发起连接,被动套接字会调用accept()函数接受连接。
write() 的原型为:
1
| ssize_t write(int fd, const void *buf, size_t nbytes);
|
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。
read() 的原型为:
1
| ssize_t read(int fd, void *buf, size_t nbytes);
|
fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。
close函数是用来关闭套接字
成功返回0,出错为-1
实现一对一的客户/服务器回射:
echosrv.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0); int main(int argc, char** argv) { int listenfd; if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof servaddr); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; // 确保time_wait状态下同一端口仍可使用 if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0) { ERR_EXIT("setsockopt"); } */ if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0) { ERR_EXIT("bind"); } if (listen(listenfd, SOMAXCONN) < 0) { ERR_EXIT("listen"); } struct sockaddr_in peeraddr; socklen_t peerlen = sizeof peeraddr; int connfd; if ((connfd = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) { ERR_EXIT("accept"); } printf("id = %s, ", inet_ntoa(peeraddr.sin_addr)); printf("port = %d\n", ntohs(peeraddr.sin_port)); char recvbuf[1024]; while (1) { memset(recvbuf, 0, sizeof recvbuf); int ret = read(connfd, recvbuf, sizeof recvbuf); if (ret == 0) { } else { } fputs(recvbuf, stdout); write(connfd, recvbuf, ret); } close(connfd); close(listenfd); return 0; }
|
echocli.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0); int main(int argc, char** argv) { int sock; if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { ERR_EXIT("socket"); } struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof servaddr); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("connect lalala"); char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL) { write(sock, sendbuf, strlen(sendbuf)); read(sock, recvbuf, sizeof(recvbuf)); fputs(recvbuf, stdout); memset(sendbuf, 0, sizeof(sendbuf)); memset(recvbuf, 0, sizeof(recvbuf)); } close(sock); return 0; }
|
Makefile
1 2 3 4 5 6 7 8 9
| .PHONY:clean all CC=gcc CFLAGS=-Wall -g BIN=echosrv echocli all:$(BIN) %.o:%.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o $(BIN)
|
首先启动服务器echosrv,再启动客户端echocli。在客户端发送一行消息后,服务器会收到一条消息,随后服务器又会把消息原封不动的发回客户端。
即在客户端发送一行消息,执行的是如图所示的过程
补充:send()/recv()和write()/read():发送数据和接收数据
在 Linux 和 Windows 平台下,使用不同的函数发送和接收 socket 数据,下面我们分别讲解。
Linux下数据的接收和发送
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。
前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
1
| ssize_t write(int fd, const void *buf, size_t nbytes);
|
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 “size_t” 前面加了一个”s”,代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。
1
| ssize_t read(int fd, void *buf, size_t nbytes);
|
fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。
Windows下数据的接收和发送
Windows 和 Linux 不同,Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数。
从服务器端发送数据使用 send() 函数,它的原型为:
1
| int send(SOCKET sock, const char *buf, int len, int flags);
|
sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。
返回值和前三个参数不再赘述,最后的 flags 参数一般设置为 0 或 NULL,初学者不必深究。
在客户端接收数据使用 recv() 函数,它的原型为:
1
| int recv(SOCKET sock, char *buf, int len, int flags);
|