一.题目:
1 .实验目的
熟悉和掌握网络编程的基本方法和步骤;
进一步理解client/server 交互模式;
加深学生对于网络协议概念的理解以及协议的设计和实现方法
2 .实验任务
使用任意网络编程语言( Java 、 C 、 VB 、 Delphi 等)编写网络选课模拟程序,它由 client 和 server 两部分组成, client 和 server 之间的通信基于 TCP 协议,并且实现 NCSP 应用层协议( Network-based Course Selection Protocol )。
3 . NCSP 应用层协议
3.1 NCSP Server 的功能
当接收到 client 发送的 GETCOURSE协议消息,返回该用户已经选择的所有课程名字;
当接收到 client 发送的 SETCOURSE协议消息,记录用户名和课程名,以便 client 使用 GETCOURSE 命令查询;
当接收到 client 发送的 SHUTDOWN 协议消息,检查用户是否有此权限,如果有则结束 server 程序。
3.2 NCSP Client 的功能
允许用户选择要发送的协议消息及其附带参数,然后向 server 发送 GETCOURSE 、 SETCOURSE 或者 SHUTDOWN 命令之一,并从 server 得到应答结果,显示给用户。
3.3 NCSP 协议
本次实验所要实现的网络选课程序,其核心是 client 和 server 之间所遵从的共同协议: NCSP 协议。下面我们详细描述该协议。
3.3.1 GETCOURSE
GETCOURSE 命令由 client 发送给 server ,它由一个 ASCII 字符串组成:首先是“GETCOURSE ”,然后紧跟着一个空格( space ),然后是用户名,最后是换行符( '\n' )。 client 然后等待服务器应答:如果返回的是“ 401 User does not exist ”字符串 ,说明该用户还没有选择课程;如果返回的是“200 OK ”字符串,说明该用户已经选择了课程, client 继续接收 server 发送的课程消息,每一门课程名是一个字符串,当接收到“ . ”字符串时,说明课程名发送完毕。 Client 断开连接,重新接收用户输入。
当 server 接收到 GETCOURSE 命令,它解析出用户名,然后检索该用户是否已经选择了课程,如果没有,则返回“ 401 User does not exist ”字符串,关闭连接;如果用户选择了课程,则返回“ 200 OK ”字符串,然后将用户选择的课程名返回给 client ,由于用户可能选择了多门课程,因此每一门课程名作为一个字符串返回给 client ,当课程名都发送完毕,发送“ . ”字符串作为结束标志。然后断开连接,重新监听新的 client 的连接请求。
当 server 解析 GETCOURSE 命令,发生错误时,返回“ 300 Message format error ”字符串。
下面是 client 和 server 的交互:
C: GETCOURSE XT
S: 200 OK
S: Computer Network
S: Database Principle
S: Java Language
S: .
3.3.2 SETCOURSE
SETCOURSE 命令由 client 发送给 server ,它由一个 ASCII 字符串组成:首先是“SETCOURSE ”,然后紧跟着一个空格( space ),然后是用户名,最后是换行符( '\n' )。 client 然后等待服务器应答:如果返回的是“ 301 User exists ”字符串,说明该用户已经选择了课程(这里我们假设用户必须一次选择好课程,不能更改);如果返回的是“ 200 OK ”字符串,说明该用户还没有选择课程,可以选课。 Client 将用户输入的课程名发送给 server ,每一门课程名是一个以换行符 '\n' 终结的字符串,当课程名都发送完毕,发送“ . ”字符串作为结束标志。然后 Client 断开连接,重新接收用户输入。
当 server 接收到 SETCOURSE 命令,它解析出用户名,然后检索该用户是否已经选择了课程,如果已经选择,则返回“ 301 User exists ”字符串,关闭连接;如果用户还没有选择课程,则返回“ 200 OK ”字符串, server 继续接收 client 发送的课程消息,每一门课程名是一个字符串,当接收到“ . ”字符串时,说明课程名发送完毕。 Server 将用户名和其选择的所有课程存储起来,然后断开连接,重新监听新的 client 的连接请求。
当 server 解析 SETCOURSE 命令,发生错误时,返回“ 300 Message format error ”字符串。
下面是 client 和 server 的交互:
C: SETCOURSE XT
S: 200 OK
C: Computer Network
C: Database Principle
C: Java Language
C: .
3.3.3 SHUTDOWN
SHUTDOWN 命令由 client 发送给 server ,它由一个 ASCII 字符串组成:首先是“SHUTDOWN ”,然后紧跟着一个空格( space ),然后是用户名,最后是换行符( '\n' )。 client 发送完毕后,接收 server 的响应结果,显示在屏幕上,然后关闭连接。
当 server 接收到 SHUTDOWN 命令,解析出用户名,如果此用户没有关闭 server 的权限,则返回“ 201 User not allowed to execute this command ”字符串;如果此用户具有关闭 server 的权限,则返回“ 200 OK ”字符串,然后关闭连接,结束 server 程序。
当 server 解析 SHUTDOWN 命令,发生错误时,返回“ 300 Message format error ”字符串。
下面是 client 和 server 的交互:
C: SHUTDOWN XT
S: 200 OK
4 .编程提示
可以采用文本或者图形界面(GUI )进行输入和输出;
协议命令的打包和解析是编制程序要考虑的关键问题之一;
server 采用什么样的数据结构存储用户及其所选择的课程名也是编制程序要考虑的关键问题之一。为了简化程序,可以规定一个用户最多选择的课程数。
5 .实验报告提交内容
server 源程序和 client 源程序,并且加上必要的注释(以便表明你理解代码的含义)
实验报告
6 .实验成绩评分标准
正确性 (70%)
文档和注释 (20%)
程序风格 (10%)
二.整体思路:
在文件中存储用户和课程信息,每一行存储一个用户或课程信息,然后从SETCOURSE命令为切入点,因为要先SET然后才能GET,然后一步一步的添加功能。对于权限问题,我采取的是随机生成一个0或1,然后加到用户名后面,0为特权用户,1为普通用户。
三.注意点:
在成功的运行了第一次之后,再次启动服务器端程序时,./server就会变得邪恶起来,在bind()这个函数中会出现了Address already in use这个错误。当出现这个错误时,我在百度上搜到的资料如下:
http://www.ibm.com/developerworks/cn/linux/l-sockpit/。
bind 普遍遭遇的问题是试图绑定一个已经在使用的端口。该陷阱是也许没有活动的套接字存在,但仍然禁止绑定端口(bind 返回 EADDRINUSE),它由 TCP 套接字状态 TIME_WAIT 引起。该状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。
等待 TIME_WAIT 结束可能是令人恼火的一件事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。
考虑清单 3 的例子。在绑定地址之前,我以 SO_REUSEADDR 选项调用 setsockopt。为了允许地址重用,我设置整型参数(on)为 1 (不然,可以设为 0 来禁止地址重用)。
即在服务器程序中,在创建一个套接字之后,在绑定本地地址之前,调用下面的函数:
1
2
3
4
5
|
if
(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse,
sizeof
(reuse)) < 0)
{
perror
(
"setsockopet"
);
return
-1;
}
|
四.程序运行结果:
五.相关代码:
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
|
1
/****************************************
2 > File Name:server.c
3 > Author:xiaoxiaohui
4 > mail:1924224891@qq.com
5 > Created Time:2016年05月15日 星期日 16时06分03秒
6 ****************************************/
7
8 #include<stdio.h>
9 #include<stdlib.h>
10 #include<sys/types.h>
11 #include<sys/socket.h>
12 #include<unistd.h>
13 #include <arpa/inet.h>
14 #include<netinet/in.h>
15 #include<fcntl.h>
16 #include<string.h>
17 #include<fcntl.h>
18 #include<pthread.h>
19
20 #define LEN 1024
21
const
int
PORT = 8080;
22
int
listenSock, linkSock;
23
struct
sockaddr_in lockal;
24
struct
sockaddr_in client;
25
char
buf[LEN];
26 pthread_t tid;
27 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//互斥锁 因为buf是全局的,所以只能有一个线程访问buf
28
29
30
void
ListenSock()
//建立一个监听套接字
31 {
32 listenSock = socket(AF_INET, SOCK_STREAM, 0);
//返回一个文件描述符
33
34 lockal.sin_family = AF_INET;
35 lockal.sin_addr.s_addr = htonl(INADDR_ANY);
//适合多网卡
36 lockal.sin_port = htons(PORT);
37
38
int
on = 1;
39
if
( (setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, &on,
sizeof
(on))) < 0)
//容许端口重用
40 {
41
perror
(
"setsockopt "
);
42
exit
(EXIT_FAILURE);
43 }
44
45
if
( bind(listenSock, (
struct
sockaddr*)&lockal,
sizeof
(lockal)) < 0)
//绑定本地地址
46 {
47
perror
(
"bind"
);
48
exit
(0);
49 }
50
51
if
( listen(listenSock, 5) < 0)
//进入监听状态
52 {
53
perror
(
"listen"
);
54
exit
(1);
55 }
56 }
57
58
int
LinkSock()
//返回一个已链接套接字
59 {
60
int
size =
sizeof
(lockal);
61 linkSock = accept(listenSock, (
struct
sockaddr*)&client, &size);
//创建一个已链接套接字
62
if
(linkSock < 0)
63 {
64
perror
(
"accept"
);
65
return
-1;
66 }
67
68
return
linkSock;
69 }
70
71
int
checkUser(
char
* ptr)
//判断文件中是否有字符串ptr 有则返回1 无则返回0
72 {
73
if
(ptr == NULL)
74 {
75
printf
(
"checkUser parameter is error\n"
);
76
return
-1;
77 }
78
79
while
(*ptr !=
' '
)
80 {
81 ptr++;
82 }
83 ptr++;
//此时ptr指向用户名
84
85
FILE
* fp =
fopen
(
"message.txt"
,
"r"
);
86
if
(fp == NULL)
87 {
88
printf
(
"fopen is error!\n"
);
89
exit
(3);
90 }
91
char
* message = NULL;
92
char
buf[LEN];
93
while
( (message =
fgets
(buf, LEN, fp)) != NULL)
94 {
95
if
(
strstr
(message, ptr) != NULL)
//判断ptr是否为message的子串
96 {
97
fclose
(fp);
98
return
1;
99 }
100 }
101
fclose
(fp);
102
printf
(
"there is not exist this user!\n"
);
103
return
0;
104 }
105
106
int
checkMode(
char
* ptr)
//检查命令格式 如果返回1则代表格式正确 否则格式错误
107 {
108
if
(ptr == NULL)
109 {
110
printf
(
"checkMode parameter is error\n"
);
111
return
-1;
112 }
113
114
int
count = 0;
115
while
(*ptr !=
'\0'
)
// 判断ptr中有多少个空格字符
116 {
117
if
(*ptr ==
' '
)
118 {
119 count++;
120 }
121 ptr++;
122 }
123
return
count;
124 }
125
126
void
setcourse()
127 {
128
if
( checkMode(buf) == 1)
//如果只存在一个空格符则命令格式匹配
129 {
130
if
( checkUser(buf) == 1)
//用户名存在 说明已经选择了课程
131 {
132
char
message[] =
"301 User exist"
;
133 write(linkSock, message,
strlen
(message));
134 }
135
else
//用户名不存在,说明还没选择课程
136 {
137
FILE
* fp =
fopen
(
"message.txt"
,
"a"
);
//以追加的方式打开文件
138
fputs
(buf,fp);
//把命令和用户名写到文件中
139
140
int
root = 0;
// 以linux为参考 0表示超级用户 1表示普通用户
141 root =
rand
() % 1;
//随机生成0或1
142
if
(root == 0)
143 {
144
fputc
(
'0'
, fp);
145 }
146
else
147 {
148
fputc
(
'1'
, fp);
149 }
150
fputc
(
'\n'
, fp);
//一行只写一个内容
151
152
char
message[] =
"300 OK"
;
153 write(linkSock, message,
strlen
(message));
154
155
while
(1)
//把客户端发来的课程写到文件中
156 {
157
int
ret = 0;
158
memset
(buf,
'\0'
, LEN);
159 ret = read(linkSock, buf, LEN);
160 buf[ret] =
'\0'
;
161
162
fputs
(buf, fp);
163
fputc
(
'\n'
, fp);
164
165
if
(
strcmp
(buf,
"."
) == 0)
166 {
167
break
;
168 }
169 }
170
171
fclose
(fp);
172 }
173 }
174
else
//命令格式不匹配
175 {
176
char
message[] =
"300 Message format error"
;
177 write(linkSock, message,
strlen
(message));
178 }
179
180 }
181
182
void
getcourse()
183 {
184
char
* username = buf;
185
while
(*username !=
' '
)
186 {
187 username++;
188 }
189 username++;
//此时username指向用户名
190
191
if
( checkMode(buf) == 1)
//如果只存在一个空格符则命令格式匹配
192 {
193
if
( checkUser(buf) == 0)
//用户名不存在
194 {
195
char
message[] =
"401 User does not exist"
;
196 write(linkSock, message,
strlen
(message));
197 }
198
else
//用户名存在,说明选择了课程
199 {
200
FILE
* fp =
fopen
(
"message.txt"
,
"r"
);
//以读的方式打开文件
201
char
message[] =
"200 OK"
;
202 write(linkSock, message,
strlen
(message));
203
204
char
* content = NULL;
205
while
( (content =
fgets
(buf, LEN, fp)) != NULL)
//把课程传到客户端
206 {
207
if
(
strstr
(content, username) != NULL)
//如果读到一行中的子字符串与用户名相同,则把接下来的字符串传到客户端
208 {
209
memset
(buf,
'\0'
, LEN);
210
while
(
fgets
(buf, LEN, fp) != NULL)
//把接下来的字符串传到客户端
211 {
212 write(linkSock, buf,
strlen
(buf));
213
if
(
strstr
(buf,
"."
) == 0)
//如果遇到 . 则代表课程已经传送完
214 {
215
memset
(buf,
'\0'
, LEN);
216
break
;
217 }
218
memset
(buf,
'\0'
, LEN);
219 }
220
221
break
;
222 }
223 }
224 }
225 }
226 }
227
228
void
ShutDown()
229 {
230
if
( checkMode(buf) == 1)
//如果只存在一个空格符则命令格式匹配
231 {
232
char
* message = NULL;
233
char
buf[LEN];
234
FILE
* fp =
fopen
(
"message.txt"
,
"r"
);
235
236
char
* username = buf;
237
while
(*username !=
' '
)
238 {
239 username++;
240 }
241 username++;
//此时username指向用户名
242
243
while
( (message =
fgets
(buf, LEN, fp)) != NULL)
244 {
245
if
(
strstr
(message, username) != NULL)
//找到有此用户的一行 然后看用户后面的权限
246 {
247
if
(
strchr
(message,
'0'
) != NULL)
//权限为超级权限,则可以关闭服务器
248 {
249
fclose
(fp);
250
exit
(11);
251 }
252
else
253 {
254
break
;
255 }
256 }
257 }
258
259
fclose
(fp);
260
char
mes[] =
"201 user not allowed to execute this command"
;
//其它情况都是没有权限关闭服务器
261 write(linkSock, mes,
strlen
(mes));
262
263 }
264
else
//命令格式不匹配
265 {
266
char
message[] =
"300 Message format error"
;
267 write(linkSock, message,
strlen
(message));
268 }
269 }
270
271
void
pthread(
int
count)
272 {
273
// pthread_detach(pthread_self());
274
// while(1)
275 {
276
// pthread_mutex_lock(&lock); //加锁 因为buf是一个全局变量,也可以把buf改为局部变量, 然后把buf传给
277
// 相应的函数,这样就可以不用加锁,性能更高,此处就不改了
278
// int ret = 0;
279
// ret = read(linkSock, buf, LEN);
280
// if(ret < 0)
281
// {
282
// perror("read");
283
// continue;
284
// }
285
// buf[ret] = '\0';
286
// printf("%s\n", buf);
287
288
if
(
strstr
(buf,
"SETCOURSE"
) != NULL)
289 {
290 setcourse();
291 close(linkSock);
//关闭链接
292
// break;
293 }
294
else
if
(
strstr
(buf,
"GETCOURSE"
) != NULL)
295 {
296 getcourse();
297 close(linkSock);
//关闭链接
298
// break;
299 }
300
else
if
(
strstr
(buf,
"SHUTDOWN"
) != NULL)
301 {
302 ShutDown();
303 close(linkSock);
//关闭链接
304
// break;
305 }
306
307
// pthread_mutex_unlock(&lock);
308 }
309
310
printf
(
"the %dth of user exit!\n"
,count);
311
return
NULL;
312 }
313
314
int
main()
315 {
316 ListenSock();
//进入监听状态
317
318
int
count = 1;
//用户链接数
319
while
(1)
320 {
321
if
( LinkSock() >= 0)
//文件描述符从0开始
322 {
323
int
ret = 0;
324 ret = read(linkSock, buf, LEN);
325
if
(ret < 0)
326 {
327
perror
(
"read"
);
328
continue
;
329 }
330 buf[ret] =
'\0'
;
331
printf
(
"%s\n"
, buf);
332
333
// int error = pthread_create(&tid, NULL, pthread, (void*)count);
334
// if( error != 0 ) //创建线程失败则继续等待链接请求
335
// {
336
// printf("pthread_create is error\n");
337
// continue;
338
// }
339
340 pthread(count);
341 count++;
342
343
// if( pthread_join(tid, NULL) == 0) //等待线程终止 不能用pthread_join因为它是阻塞式的等待
344
// { //从而会停在这不会往下继续执行,就满足不了多用户同时存在
345
// printf("the %dth of user exit!\n");
346
// }
347 }
348 }
349
350
return
0;
351 }
352
353
354
355
|
client.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
103
104
105
106
107
108
109
110
111
112
113
114
|
1
/****************************************
2 > File Name:client.c
3 > Author:xiaoxiaohui
4 > mail:1924224891@qq.com
5 > Created Time:2016年05月15日 星期日 16时48分21秒
6 ****************************************/
7
8
9 #include<stdio.h>
10 #include<stdlib.h>
11 #include<sys/types.h>
12 #include<sys/socket.h>
13 #include<unistd.h>
14 #include <arpa/inet.h>
15 #include<netinet/in.h>
16 #include<fcntl.h>
17 #include<string.h>
18 #include<fcntl.h>
19
const
int
PORT = 8080;
20
const
int
LEN = 1024;
21
struct
sockaddr_in server;
22
int
clientSock;
23
24
25
void
LinkSock()
//创建一个以链接套接字
26 {
27 clientSock = socket(AF_INET, SOCK_STREAM, 0);
28
29 server.sin_family = AF_INET;
30 server.sin_addr.s_addr = inet_addr(
"127.0.0.1"
);
31 server.sin_port = htons(PORT);
32
if
( connect(clientSock, (
struct
sockaddr*)&server,
sizeof
(server)) < 0)
33 {
34
perror
(
"connect"
);
35
exit
(0);
36 }
37
else
38 {
39
printf
(
"connect success! ip:%d port:%d\n"
, server.sin_addr.s_addr, PORT);
40 }
41 }
42
43
int
main()
44 {
45
printf
(
"........................................................\n"
);
46
printf
(
"........................................................\n"
);
47
printf
(
"............... you can chose ................\n"
);
48
printf
(
"............... GETCOURSE ................\n"
);
49
printf
(
"............... SETCOURSE ................\n"
);
50
printf
(
"............... SHUTDOWN ................\n"
);
51
printf
(
"........................................................\n"
);
52
printf
(
"........................................................\n"
);
53
54
char
buf[LEN];
55
56
while
(1)
57 {
58 LinkSock();
//得到一个已链接套接字
59
60
int
ret = 0;
61
printf
(
"请选择->"
);
62
// scanf("%[^\n]", &buf); //%s 不能接受有空格的字符串 或者用gets函数
63
gets
(buf);
64
65 write(clientSock, buf,
strlen
(buf));
//把选择的命令发到服务器
66
67
memset
(buf,
'\0'
, LEN);
68 ret = read(clientSock, buf, LEN - 1 );
//接受服务器的信息
69 buf[ret] =
'\0'
;
70
printf
(
"%s"
, buf);
71
printf
(
"\n"
);
72
73
if
(
strstr
(buf,
"300 OK"
) != NULL)
74 {
75
while
(1)
76 {
77
printf
(
"请输入课程名->"
);
78
gets
(buf);
79 write(clientSock, buf,
strlen
(buf));
80
if
(
strcmp
(buf,
"."
) == 0)
//如果是 . 则课程发送完毕 退出循环
81 {
82
break
;
83 }
84
85
memset
(buf,
'\0'
, LEN);
86 }
87
88 close(clientSock);
89
memset
(buf,
'\0'
, LEN);
90 }
91
else
if
(
strstr
(buf,
"200 OK"
) != NULL)
92 {
93
while
(1)
94 {
95
memset
(buf,
'\0'
,LEN);
96 ret = read(clientSock, buf, LEN - 1 );
//接受服务器的信息
97 buf[ret] =
'\0'
;
98
printf
(
"%s"
, buf);
99
printf
(
"\n"
);
100
if
(
strstr
(buf,
"."
) == 0)
101 {
102
memset
(buf,
'\0'
, LEN);
103
break
;
104 }
105 }
106
107 close(clientSock);
108 }
109 }
110
111
112
return
0;
113 }
114
|
Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
1
2
3 .PHONY:all
4 all:server client
5
6 server:server.c
7 gcc -o $@ $^ -g -pthread
8 client:client.c
9 gcc -o $@ $^ -g -pthread
10
11 .PHONY:clean
12 clean:
13 rm -f server
14 rm -f client
|
六.总结:
网络程序出现的问题比较多,一定要通过调试来查出错误的地方,千万不能想着通过看代码看出问题的地方。遇到一个比较复杂的问题时,可以先分割问题,找到切入点,然后一步一步的实现。