socket编程学习笔记(2)

在这里插入图片描述
在这里插入图片描述

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函数是用来关闭套接字

1
int close(int sockfd);

成功返回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
//
// Created by hh on 20-05-08.
//
#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) {
// 1. 创建套接字
int listenfd;
//三个参数分别是通信协议族,socket类型,协议类型
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); //端口号,端口号是2个字节,htons中的s(short)就表示32位
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //ip地址,INADDR_ANY表示本机的任一地址
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //显式指定ip地址
// inet_aton("127.0.0.1", &servaddr.sin_addr); //和上一个效果相同
/*
int on = 1;
// 确保time_wait状态下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0)
{
ERR_EXIT("setsockopt");
}
*/
// 2. 绑定套接字地址
//三个参数分别是socket返回的套接字,要绑定的地址,地址长度
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr) < 0) {
ERR_EXIT("bind");
}
// 3. 等待连接请求状态
//两个参数分别是socket返回的套接字,规定内核为此套接字排队的最大连接个数,可以填个数字
if (listen(listenfd, SOMAXCONN) < 0) { //SOMAXCONN表示队列的最大值
ERR_EXIT("listen");
}
//定义一个对等方的套接字地址
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
// 4. 允许连接
int connfd;
//功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
//可以这么理解,peeraddr有一个ip地址,很多端口,而连接的对象是ip地址和端口组成都套接字
//返回的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));
// 5. 数据交换
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
//从connfd中读取sizeof(recvbuf)字节到把缓冲区recvbuf中,成功则返回写入的字节数,失败则返回 -1。
int ret = read(connfd, recvbuf, sizeof recvbuf);
if (ret == 0)
{
} else
{
}
fputs(recvbuf, stdout);
//把缓冲区recvbuf中的ret个字节写入connfd
write(connfd, recvbuf, ret);
}
// 6. 断开连接
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
//
// Created by hh on 20-05-08.
//
#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) {
// 1. 创建套接字
int sock;
//三个参数分别是通信协议族,socket类型,协议类型
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); //端口号,端口号是2个字节,htons中的s(short)就表示32位
//127.0.0.1是一个回送地址,指本地机,一般用来测试使用。
//常用来ping 127.0.0.1来看本地ip/tcp正不正常,如能ping通即可正常使用。
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //显式指定ip地址
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() 从套接字中读取出来,就完成了一次通信。

  • write() 的原型为:
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。

  • read() 的原型为:
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);