在上节已经系统介绍了大致的流程和相关的API,这节就开始写代码!
回顾上节的流程:
创建一个NET文件夹 来存放网络编程相关的代码:
tcp服务端代码初步实现--上
这部分先实现服务器的连接部分的代码并进行验证
server1.c:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sockfd;
int conn_sockfd;
int ret;
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
memset(&my_addr,0,sizeof(struct sockaddr_in));
memset(&client_addr,0,sizeof(struct sockaddr_in));
//socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return 1;
}else{
printf("socket success, sockfd = %d\n",sockfd);
}
//bind
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);//host to net (2 bytes)
inet_aton("192.168.20.137",&my_addr.sin_addr); //char* format -> net format
ret = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind");
return 1;
}else{
printf("bind success\n");
}
//listen
ret = listen(sockfd,10);
if(ret == -1){
perror("listen");
return 1;
}else{
printf("listening...\n");
}
//accept
int len = sizeof(struct sockaddr_in);
conn_sockfd = accept(sockfd,(struct sockaddr *)&client_addr,&len);
if(conn_sockfd == -1){
perror("accept");
return 1;
}else{
printf("accept success, client IP = %s\n",inet_ntoa(client_addr.sin_addr));//将网络格式的IP地址再转回字符串
}
//read
//write
//read
//close
return 0;
}
代码验证:
先编译并运行这部分代码:
可见,此时没有客户端进行连接,程序会阻塞在监听的阶段
此时打开windows的cmd(windows系统和linux虚拟机的系统可以看作两台不同的终端)
telnet指令使用的也是TCP协议
执行这条命令后,windows的cmd变成了这样:
再反观linux虚拟机:
使用windows的ipconfig可以验证IP地址 :
所以,连接部分的代码已经成功,只是因为没有接下来的数据传输所以退出了。
tcp服务端代码初步实现--下
这部分实现服务器的连接成功后的读写并验证
server1.c:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sockfd;
int conn_sockfd;
int ret;
int n_read;
int n_write;
char readbuf[1024];
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
memset(&my_addr,0,sizeof(struct sockaddr_in));
memset(&client_addr,0,sizeof(struct sockaddr_in));
//socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return 1;
}else{
printf("socket success, sockfd = %d\n",sockfd);
}
//bind
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(8888);//host to net (2 bytes)
inet_aton("192.168.20.137",&my_addr.sin_addr); //char* format -> net format
ret = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("bind");
return 1;
}else{
printf("bind success\n");
}
//listen
ret = listen(sockfd,10);
if(ret == -1){
perror("listen");
return 1;
}else{
printf("listening...\n");
}
//accept
int len = sizeof(struct sockaddr_in);
conn_sockfd = accept(sockfd,(struct sockaddr *)&client_addr,&len);
if(conn_sockfd == -1){
perror("accept");
return 1;
}else{
printf("accept success, client IP = %s\n",inet_ntoa(client_addr.sin_addr));
}
//read
n_read = read(conn_sockfd,&readbuf,1024);
if(n_read == -1){
perror("read");
return 1;
}else{
printf("%d bytes has been read, context = %s\n",n_read,readbuf);
}
//write
char *msg = "this is server, I have received your msg\n";
n_write = write(conn_sockfd,msg,strlen(msg));
if(n_write == -1){
perror("write");
return 1;
}else{
printf("%d bytes has been written\n",n_write);
}
//read
//close
return 0;
}
代码验证:
这部分如果用windows的telnet,打一个符号就会直接结束,所以用Linux另开一个cmd使用telnet来模拟服务器和客户端的对话:
还是先运行代码:
然后运行telnet:
反观服务端:
在连接成功后客户端输入一句话然后回车:
客户端显示:
服务器显示:
可见,数据的交互基本没什么问题。
至此,构建了一个大致的服务器代码框架,可以开始着手编写客户端的代码了。
客户端代码初步实现
client.c:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sockfd;
int ret;
int n_read;
int n_write;
char readbuf[1024];
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(struct sockaddr_in));
//socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return 1;
}else{
printf("socket success, sockfd = %d\n",sockfd);
}
//connect
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);//host to net (2 bytes)
inet_aton("192.168.20.137",&server_addr.sin_addr);
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("connect");
return 1;
}else{
printf("connect success!\n");
}
//write
char *msg = "client: hello\n";
n_write = write(sockfd,msg,strlen(msg));
if(n_write == -1){
perror("write");
return 1;
}else{
printf("%d bytes has been written\n",n_write);
}
//read
n_read = read(sockfd,&readbuf,1024);
if(n_read == -1){
perror("read");
return 1;
}else{
printf("%d bytes has been read, context = %s\n",n_read,readbuf);
}
//close
return 0;
}
代码验证:
先编译并允许服务端:
再编译并运行客户端:
回看服务端:
客户端 & 服务端 代码的最终实现
之前,已经大致编写出了客户端和服务端的代码,但是和本节开头的框图对比,发现还差一些,首先,数据的发送和读取应该持续进行,直到收到结束信号 ,另外在最后要编写关闭套接字,关闭连接的代码 ,且服务器应该可以接受多个客户端的数据 ,还有一些细节的优化:
回顾之前关于fork函数的一节:进程的创建_mjmmm的博客-CSDN博客
所以此处可以使用fork函数!
小知识点:自动对齐(gg=G)
(参考:linux代码对齐快捷键和man帮助文档的使用总结_陌上花开缓缓归以的博客-CSDN博客)
在命令模式下(即非"插入"等编辑模式),先输入gg,这时候光标会移动到第一行第一个字符,然后按 "=" 号之后切换成大写,再按一下G,这时候光标会移到最后一行的第一个字符,这时候就可以看到代码被排得整整齐齐了!
"gg"将光标移动到代码首部,"="表示对齐指令,"G"表示代码尾部,所以执行"gg=G"后,该文件的所有代码都将对齐!
实现思路
服务端:
在listen之后,进入一个while(1)循环 ,并调用accept阻塞,一旦连接到一个客户端,就fork一个子进程来处理数据,父进程则继续通过while(1)继续调用accept阻塞等待下一个客户端连接。
而子进程中再次调用fork,父进程写一个while(1)不断的写数据,子进程写一个while(1)不断的读数据。
客户端:
在connect之后进行fork,父进程写一个while(1)不断的写数据,子进程写一个while(1)不断的读数据。
如何退出:
我希望的退出方式是客户端输入"quit"就会退出 ,但是不管是客户端还是服务端,为了读和写不会相互阻塞,都在不同的进程中的while(1)里,当客户端输入"quit"之后,只有客户端的写端和服务器的读端知道,客户端的读端和服务器的写端并不知情 ,所以需要使用进程间的通讯,此处我使用了FIFO
在客户端中 创建FIFO并在不断写数据的父进程中不断检测是否输入了"quit" ,如果是就只写打开fifo,并阻塞等待....一旦等待到了有进程只读打开FIFO,就会往FIFO写入"quit",然后关闭FIFO;关闭套接字;收集子进程退出状态;然后退出循环;正常退出。 同时在不断读数据的子进程中不断非阻塞的只读打开fifo,并每次都将光标移到最开头,一旦从FIFO读取到了"quit",就exit。
但是,不断读数据的子进程会阻塞读取服务器传来(写入)的数据,这就导致,当客户端输入"quit"之后无法立刻退出,而是要等到服务器再发来消息,才能进行下一轮的FIFO读取,才能使得子进程收到父进程通过FIFO发来的"quit"并退出。解决办法就是:在服务器端中,一旦检测到客户端发来的消息是quit之后,就立刻给客户端发送一句话
此时,对于客户端来说输入了"quit"之后会立刻退出,但是服务端只有读端的while可以退出,写端的while无法退出 ,此时就有一个疑问"我在读端关闭了客户端套接字,照理说写端应该往这个套接字里写会报错,我直接在报错处理函数里退出写端不就行了",但其实这是行不通的,因为文件描述符的作用域默认情况下只在进程内有效,而无法在进程之间进行传递。所以还是需要使用FIFO,且注意,FIFO是进程与进程间的,客户端和服务端本质也属于两个进程,所以服务端如果要使用FIFO应该在mkfifo函数中对于FIFO的名字修改,不要和服务端的FIFO重名
在服务器端创建另一个FIFO并在不断读数据的子进程中不断判断是否从客户端收到了"quit",如果收到了就立刻回复(写)一个"Bye"(也就是为了让客户端能立刻退出);只写打开FIFO,并阻塞等待.... 一旦等待到了有进程只读打开FIFO,就会往FIFO写入"quit",然后exit。 同时在不断写数据的父进程中不断非阻塞的只读打开fifo,并每次都将光标移到最开头,一旦从FIFO读取到了"quit",就关闭FIFO;关闭客户端的套接字;收集子进程退出状态;然后退出循环;执行fork之后的exit。
程序框图:
server_final.c:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int conn_num = 0;
int flag = 0;
int sockfd;
int conn_sockfd;
int ret;
int n_read;
int n_write;
int len = sizeof(struct sockaddr_in);
char readbuf[128];
char msg[128];
int fd; //fifo
char fifo_readbuf[20] = {0};
char *fifo_msg = "quit";
pid_t fork_return;
pid_t fork_return_1;
struct sockaddr_in my_addr;
struct sockaddr_in client_addr;
memset(&my_addr,0,sizeof(struct sockaddr_in));
memset(&client_addr,0,sizeof(struct sockaddr_in));
if(argc != 3){
printf("param error!\n");
return 1;
}
//socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return 1;
}else{
printf("socket success, sockfd = %d\n",sockfd);
}
//bind
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[2]));//host to net (2 bytes)
inet_aton(argv[1],&my_addr.sin_addr); //char* format -> net format
ret = bind(sockfd, (struct sockaddr *)&my_addr, len);
if(ret == -1){
perror("bind");
return 1;
}else{
printf("bind success\n");
}
//listen
ret = listen(sockfd,10);
if(ret == -1){
perror("listen");
return 1;
}else{
printf("listening...\n");
}
//fifo
if(mkfifo("./fifo1",S_IRWXU) == -1 && errno != EEXIST)
{
perror("fifo");
}
while(1){
//accept
conn_sockfd = accept(sockfd,(struct sockaddr *)&client_addr,&len);
if(conn_sockfd == -1){
perror("accept");
return 1;
}else{
conn_num++;
if(conn_num > 1){
printf("there are more then one client, msg may not be sent accuratly!\n");
}
printf("accept success, no.%d client IP = %s\n",conn_num,inet_ntoa(client_addr.sin_addr));
}
fork_return = fork();
if(fork_return > 0){//father keeps waiting for new request
//wait(NULL); //cant wait,will block
}else if(fork_return < 0){
perror("fork");
return 1;
}else{//son deals with request
fork_return_1 = fork();
if(fork_return_1 > 0){//father keeps writing msg
while(1){
fd = open("./fifo1",O_RDONLY|O_NONBLOCK);
lseek(fd, 0, SEEK_SET);
read(fd,&fifo_readbuf,20);
//printf("read from fifo:%s\n",fifo_readbuf);
if(fifo_readbuf[0]=='q' && fifo_readbuf[1]=='u' && fifo_readbuf[2]=='i' && fifo_readbuf[3]=='t'){
printf("sorry,the last msg sent fail,client has quit\n");
close(fd);
close(conn_sockfd);
wait(NULL);
break;
}
//write
memset(&msg,0,sizeof(msg));
//printf("\ntype msg:");
scanf("%s",(char *)msg);
n_write = write(conn_sockfd,&msg,strlen(msg));
if(n_write == -1){
perror("write");
return 1;
}else{
printf("%d bytes msg sent\n",n_write);
}
}
}else if(fork_return_1 < 0){
perror("fork");
return 1;
}else{//son keeps reading msg
while(1){
//read
memset(&readbuf,0,sizeof(readbuf));
n_read = read(conn_sockfd,&readbuf,128);
if(readbuf[0]=='q' && readbuf[1]=='u' && readbuf[2]=='i' && readbuf[3]=='t'){
printf("client quit\n");
conn_num--;
printf("%d client remain\n",conn_num);
write(conn_sockfd,"BYE",3);
fd = open("./fifo1",O_WRONLY);
write(fd,fifo_msg,strlen(fifo_msg));
exit(1);
}
if(n_read == -1){
perror("read");
return 1;
}else{
printf("\nclient: %s\n",readbuf);
}
}
}
exit(2);
}
}
return 0;
}
client_final.c:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/in.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int sockfd;
int ret;
int n_read;
int n_write;
char readbuf[128];
char msg[128];
int fd; //fifo
char fifo_readbuf[20] = {0};
char *fifo_msg = "quit";
pid_t fork_return;
if(argc != 3){
printf("param error!\n");
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(struct sockaddr_in));
//socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return 1;
}else{
printf("socket success, sockfd = %d\n",sockfd);
}
//connect
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[2]));//host to net (2 bytes)
inet_aton(argv[1],&server_addr.sin_addr);
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));
if(ret == -1){
perror("connect");
return 1;
}else{
printf("connect success!\n");
}
//fifo
if(mkfifo("./fifo",S_IRWXU) == -1 && errno != EEXIST)
{
perror("fifo");
}
//fork
fork_return = fork();
if(fork_return > 0){//father keeps writing msg
while(1){
//write
memset(&msg,0,sizeof(msg));
//printf("\ntype msg:");
scanf("%s",(char *)msg);
n_write = write(sockfd,&msg,strlen(msg));
if(msg[0]=='q' && msg[1]=='u' && msg[2]=='i' && msg[3]=='t'){
printf("quit detected!\n");
fd = open("./fifo",O_WRONLY);
write(fd,fifo_msg,strlen(fifo_msg));
close(fd);
close(sockfd);
wait(NULL);
break;
}
if(n_write == -1){
perror("write");
return 1;
}else{
printf("%d bytes msg sent\n",n_write);
}
}
}else if(fork_return < 0){
perror("fork");
return 1;
}else{//son keeps reading
while(1){
fd = open("./fifo",O_RDONLY|O_NONBLOCK);
lseek(fd, 0, SEEK_SET);
read(fd,&fifo_readbuf,20);
//printf("read from fifo:%s\n",fifo_readbuf);
if(fifo_readbuf[0]=='q' && fifo_readbuf[1]=='u' && fifo_readbuf[2]=='i' && fifo_readbuf[3]=='t'){
exit(1);
}
//read
memset(&readbuf,0,sizeof(readbuf));
n_read = read(sockfd,&readbuf,128);
if(n_read == -1){
perror("read");
return 1;
}else{
printf("\nserver: %s\n",readbuf);
}
}
}
return 0;
}
实现效果:
编译并运行server端:
编译并运行client端:
此时回看server端:
此时,就可以实现双方聊天的功能了:(服务器左,客户端右)
直到客户端打出"quit" :
客户端会立刻退出,服务器此时只有读端知道客户端退出了
必须再打一句话让服务器写端刷新,这样才能从FIFO读取到信息,让写端也知道客户端退出了:
此时,服务器的读写端也全部退出,再次进入阻塞状态,等待新连接...
下面模拟如果同时有两个或以上连接的时候:(左侧一个服务器,右侧两个客户端)
我代码执行的顺序是:连接第一个客户端(右上)--> 服务器发送"hi" --> 连接第二个客户端(右下)--> 服务器发送"hihi" --> 服务器发送"hihihi" --> 服务器发送"hihihihi"
可见,连接第一个客户端的时候,服务器发送hi准确的到了右上的客户端1,但是当连接第二个客户端了之后,服务器分别发送了"hihi","hihihi" ,"hihihihi" ,从服务端看没有任何区别,但实际情况下"hihi"和"hihihihi"到了客户端1,"hihihi"到了客户端2,明显出现了混乱,所以我在代码中设置了提醒,检测到大于1个客户端接入时会提醒。
问题探讨 和 一些思路过程
- 如刚刚所说,服务器用了两次fork,相当于有3个独立的进程,所以我的conn_num变量的设置实际上是相当残疾的,因为我把conn_num-- 放在了一个子进程里,而fork之后的变量空间都是独立的,所以我的conn_num变量只要有客户端推出就不准了,应该也使用进程间通信来通知,但是鉴于我的代码实现目的本来就是双人聊天,所以多客户端的连接部分就没有深入修改了
- 在我目前的代码逻辑中,我本来觉得只设置了一个conn_sockfd变量来存放客户端的套接字不合理,因为当多个客户端接入的时候,套接字可能会被覆盖,导致读写异常,但是其实一个就够了,原因也是因为fork之后变量空间也会被复制,根据我的代码逻辑,每出现一个新连接,就会fork一个子进程来处理这个连接的读写,如果有多个客户端,就会有多个子进程,里面的套接字变量名都是conn_sockfd,但其实值是不一样的。
- 并且,服务端的第一个fork,父进程不能调用wait,因为wait会阻塞直到子进程退出,但我希望父进程不被阻塞而一直while循环等待新连接,所以第一次fork生成的子进程在客户端退出之后会变成僵尸进程,且每有一个新的客户端退出就会多一个僵尸进程,在当前的逻辑下不可避免。
且,当多个客户端连接服务器的时候,服务器会针对每个客户端fork一个子进程来处理,而每一个子进程都会再fork一个读端不停的scanf检测输入。
但是进程与进程之间是竞争的关系 ,所以在cmd中看来,光标一直在闪没有变化,但实际上,可能上一秒这是客户端1对应子进程的scanf,下一秒就变成了客户端2对应子进程的scanf。
这就导致了如果此时对着光标输入消息并回车,无法确定收到消息的是客户端1还是客户端2。
- 解决办法1:使用线程,实现真正的多方数据收发,但是难度很大,需要更多的新知识
- 解决办法2:舍弃服务器的scanf功能,改为自动回复,这样可以实现多个客户端对服务器的自定义消息发送,但是服务器只能回复预设的内容
- 解决办法3:(也就是现在实现的效果)舍弃多方发送,只用一个客户端,这样就可以实现客户端和服务器的自定义消息交互,但是此时不能增加更多的客户端