linux-网络编程-使用TCP的C/S模型

基于TCP协议的客户端/服务器程序的一般流程

TCP协议下的C/S流程

服务器

调用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()则连接处于半关闭状态,仍可接收对方发来的数据。

实现

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
//server 端,作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_PORT 9726

/*错误处理函数*/
void sys_err(const char* str){
perror(str);
exit(1);
}

int main(){
int link_fd=0; //建立连接的socket文件描述符
int connect_fd=0 //用于通信的文件描述符
int ret=0; //用于检查是否出错
char buf[BUFSIZ]; //缓冲区
char client_IP[1024] //存入客户端IP字符串
int num=0; //读出的字节数
/*服务器端地址结构*/
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);

/*成功与服务器建立连接的客户端地址结构*/
struct sockaddr_in clint_addr;
socklen_t clint_addr_len=sizeof(clint_addr);

/*socket函数:创建用于建立连接的socket,返回的文件描述符存入link_fd*/
link_fd=socket(AF_INET,SOCK_STREAM,0);
if(link_fd==-1)
sys_err("socket error");

/*bind函数:绑定服务器端的地址结构*/
ret=bind(link_fd,(const struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret==-1)
sys_err("bind error");

/*listen函数:设定监听(连接)上线*/
ret=listen(link_fd,128);
if(ret==-1)
sys_err("listen error");

/*accept函数:阻塞等待客户端建立连接*/
connect_fd=accept(link_fd,(struct sockaddr*)&clint_addr,&clint_addr_len);
if(connect_fd==-1)
sys_err("accept error");
/*建立连接后打印客户端的IP和端口号*/
printf("client IP:%s,client port:%d",
inet_ntop(AF_INET,&clint_addr.sin_addr.s_addr,client_IP,sizeof(client_IP)),
ntohs(clint_addr.sin_port));

/*业务逻辑*/
while(1){
num=read(connect_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,num);
for(i=0;i<num;i++)
buf[i]=toupper(buf[i]);
write(connect_fd,buf,num);
sleep(1);
}
close(connect_fd);
close(link_fd);
return 0;
}

获取客户端的地址:

1
2
3
printf("client IP:%s,client port:%d",
inet_ntop(AF_INET,&clint_addr.sin_addr.s_addr,client_IP,sizeof(client_IP));
ntohs(clint_addr.sin_port);

client端实现:

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
//client的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERV_PORT 9726

/*错误处理函数*/
void sys_err(const char* str){
perror(str);
exit(1);
}

int main(){
int client_fd=0;
int ret=0;
int num=0;
int cnt=10;
char buf[BUFSIZ];

//connect的参数2填入服务器的文件描述符!
struct sockaddr_in serv_addr;
serv_addr.sin_family=AF_INET;
serv_addr.sin_port=htons(SERV_PORT);

/*点分十进制->网络二进制*/
/*协议族,源数据,目的数据(int32足够,因为IP地址就是32位)*/
inet_pton(AF_INET,"127.0.0.1",(void*)&serv_addr.sin_addr.s_addr);

/*客户端直接创建用于连接的套接字即可*/
client_fd=socket(AF_INET,SOCK_STREAM,0);
if(client_fd==-1)
sys_err("socket error");

/*将客户端套接字与服务器地址结构连接起来*/
ret=connect(client_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
if(ret!=0)
sys_err("connect error");

//业务逻辑
while(--cnt){
write(client_fd,"fuckyou\n",8);
num=read(client_fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,num);
sleep(1);
}
close(client_fd);
return 0;
}

注意:套接字: 一个fd可以索引读写两个缓冲区

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

客户端和服务器启动后可以使用netstat命令查看链接情况:

netstat -apn | grep 9726

出错处理

为使错误处理的代码不影响主程序的可读性,把与socket相关的一些系统函数加上错误处理代码封装成新的函数,做成一个模块wrap.c。

封装的目的:在server.c编译过程中突出逻辑,将出错处理与逻辑分开。将原函数首字母大写进行错误处理,这样还可以跳转到原函数的manPage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//wrap.h
//存放网络通信相关常用自定义函数(声明)
//在server.c 和client.c中调用自定义函数
//联合编译server.c和wrap.c生成server
//联合编译client.c和wrap.c生成client
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
#endif
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// wrap.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.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;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
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;
else
return -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;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
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;
else
return -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;
else
return -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;
} else
return -1;
}
*ptr = 0;
return n;
}

半关闭

由原来的双工通信变为了单工通信, 客户端只能接受数据(缓冲区中的数据)

实现原理:

关闭了客户端套接字的写缓冲区

  • 之所以半关闭后Client仍能向Server发送ACK数据包, 是因为Client关闭的只是写缓冲, 连接还在

  • 连接在内核层面, 写缓冲在用户层面

  • 如果Server没有收到Client最后发来的ACK数据包, 它会一直发送FIN数据包, 直到Client回执为止