TCP网络编程(二)—— 服务器端的编写
上篇文章我们学习了TCP的两种编程模式,这篇文章我们将开始编写服务器端的代码。完整代码在文章的最后。
首先,我们需要什么变量?
我们需要服务器端的套接字(socket),地址和端口(addr),还需要客户端的套接字(socket),地址和端口(addr)。
#define Port 8888
#define BACKLOG 2
int ss,sc;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int err;
pid_t pid;
这里:
ss表示服务器端套接字
sc表示客户端套接字
server_addr表示服务器端地址端口
client_addr表示客户端地址和端口
err是标志错误位,作为一些函数的返回值。
pid是进程的表示进程的ID,后面会解释,使用。
Port 是端口号,BACKLOG是监听队列的长度。
然后我们开始创建一个套接字:
ss = socket(AF_INET,SOCK_STREAM,0);
if(ss < 0){
printf("socket error \n");
return -1;
}
AF_INET 指定使用 IPv4 地址。
SOCK_STREAM 指定使用面向连接的 TCP 协议(如果是 UDP,则使用 SOCK_DGRAM)。
0 通常表示默认协议,与前两个参数相关(例如 SOCK_STREAM 默认对应 TCP 协议)。
其次是bind()绑定的准备工作(初始化服务器端的地址和端口):
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(Port);
bzero()将 server_addr 结构体初始化为 0,确保所有字段清零。避免未初始化的结构体包含随机数据,可以用 memset() 替代:memset(&server_addr, 0, sizeof(server_addr));
sin_family 是地址族字段,用于指定套接字类型。AF_INET 表示使用 IPv4 地址。
sin_addr.s_addr 设置服务器的 IP 地址,inet_addr("127.0.0.1") 将字符串形式的 IP 地址转换为 32 位整数(网络字节序)。IP 地址 "127.0.0.1" 是回环地址,表示当前主机的本地通信。
sin_port:Port 表示端口号。htons() 函数将主机字节序(小端字节序)转换为网络字节序(大端字节序),确保跨平台通信时端口号的正确性。
然后是bind(),将服务器端的套接字与地址和端口绑定到一起,
err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(err < 0){
printf("bind error \n");
return -1;
}
这里用强制类型转换,把struct sockaddr_in* 转化为struct sockaddr*,忘记了请看上篇文章。err 如果返回-1说明失败,打印错误信息。
然后是设置监听队列的长度:
err = listen(ss,BACKLOG);
if(err < 0){
printf("listen error \n");
return -1;
}
err 如果返回-1说明失败,打印错误信息。
最后最重要的主循环(accept等待请求,read读取,write写入):
for(;;){
socklen_t addrlen = sizeof(struct sockaddr);
sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
if(sc < 0){
continue;
}
pid = fork(); //开启子进程
if(pid == 0){
close(ss);
char buffer [1024] = {0};
char message [100] = {0};
ssize_t size;
for(;;){
memset(buffer, 0, sizeof(buffer));
size = read(sc,message,sizeof(message));
if(size == 0){
return 0;
}
sprintf(buffer,"server have received %s \n",message);
//write(sc,buffer,sizeof(buffer));
write(1,buffer,strlen(buffer));
}
close(sc);
}else if(pid > 0){
close(sc);
}else{
printf("fork error \n");
}
}
咱们先看fork开启子进程之前的代码:
socklen_t addrlen = sizeof(struct sockaddr);
sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
if(sc < 0){
continue;
}
pid = fork(); //开启子进程
新建addrlen变量,把套接字描述符,客户端地址和端口,客户端结构体大小传入,得到新的套接字描述符 sc , 如果 sc 小于0,直接跳出整个大的循环,这说明没有产生新的套接字。
那这个fork是什么意思呢?
fork() 是一个用于创建新进程的函数,特点是调用一次,返回两次。调用 fork() 后,当前进程会被复制,生成一个新进程,称为 子进程。相当于进程现在有两个:父进程和子进程都会收到fork()的返回值,在父进程中返回子进程的进程ID号,在子进程中返回0,失败返回-1。
现在咱们接着来看后面的代码:
for(;;){
//......
//accept()阻塞等待连接
//......
pid = fork(); //开启子进程
if(pid == 0){
close(ss);
char buffer [1024] = {0};
char message [100] = {0};
ssize_t size;
for(;;){
memset(buffer, 0, sizeof(buffer));
size = read(sc,message,sizeof(message));
if(size == 0){
return 0;
}
sprintf(buffer,"server have received %s \n",message);
//write(sc,buffer,sizeof(buffer)); 写入sc客户端套接字
write(1,buffer,strlen(buffer));
}
close(sc);
}else if(pid > 0){
close(sc);
}else{
printf("fork error \n");
}
}
首先我们要记得我们在刚刚创建了两个套接字,一个是监听之后的服务器端套接字ss,还有一个是accept()返回的客户端套接字sc。
可以看到我们在子进程中关闭了ss套接字,所以保留了客户端套接字,这说明子进程是用来和客户端保持通信连接的,在子进程中实现数据的处理,read 和write,循环处理。(read和write的用法不细说了,就是从套接字中读取或者写入,这里代码中的意思是从sc套接字中读取,写到标准输出1中,也就是打印到终端上)
在父进程中我们关闭了sc套接字,所以保留了服务器端的套接字,这说明父进程是用来监听是否有新的客户端的,等待accept()接受有效值进而创建新的子进程的,因为不要忘了外面的一层大的循环。
看到这里,你可能会想:可不可以在父进程中创建连接,在子进程中进行监听呢?
理论上是可以的,但可能会出现问题:
首先fork函数是让父进程一直产生子进程,如果颠倒,多个子进程会一直抢夺监听,造成混乱,其次让父进程和多个客户端进行通信当奔溃时服务器将无法工作。如果是父进程监听,子进程进行创建连接的话,其实子进程崩溃,父进程仍然正常运行。
最后的最后,当子进程结束,我们需要对子进程的资源进行回收:
子进程退出时,内核会向父进程发送 SIGCHLD 信号。sigchld_handler 是一个信号处理函数,用于捕捉 SIGCHLD 信号,并调用 waitpid() 非阻塞地回收所有退出的子进程。把下面的代码加入到文件中。
void sigchld_handler(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc ,char* argv[]){
signal(SIGCHLD, sigchld_handler);
return 0;
}
文章的最后是服务器端的完整代码:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define Port 8888
#define BACKLOG 2
void sigchld_handler(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc ,char* argv[]){
int ss,sc;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int err;
pid_t pid;
signal(SIGCHLD, sigchld_handler);
ss = socket(AF_INET,SOCK_STREAM,0);
if(ss < 0){
printf("socket error \n");
return -1;
}
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(Port);
err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(err<0){
printf("bind error \n");
return -1;
}
err = listen(ss,BACKLOG);
if(err<0){
printf("listen error \n");
return -1;
}
for(;;){
socklen_t addrlen = sizeof(struct sockaddr);
sc = accept(ss,(struct sockaddr*)&client_addr,&addrlen);
if(sc<0){
continue;
}
pid = fork();
if(pid == 0){
close(ss);
char buffer [1024] = {0};
char message [100] = {0};
ssize_t size;
for(;;){
memset(buffer, 0, sizeof(buffer));
size = read(sc,message,sizeof(message));
if(size == 0){
return 0;
}
sprintf(buffer,"server have received %s \n",message);
//write(sc,buffer,sizeof(buffer));
write(1,buffer,strlen(buffer));
}
close(sc);
}else if(pid > 0){
close(sc);
}else{
printf("fork error \n");
}
}
return 0;
}
希望文章对你有所帮助,如有错误欢迎指出,下篇文章,我们将编写客户端,并且实现服务器端和客户端之间的通信。