序言
- 网络字节序
C语言中,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网网络数据流应采用大端字节序,即低地址高字节
所以为了使网络程序具有可移植性,使相同的C代码在大端和小端计算机上编译正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
- 点分十进制转换函数
tcp_server.c(服务器端)
- 创建客户端:
1:创建套接字,绑定IP和端口号
2:设置为监听状态
3:accept操作
4:读写操作
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
///
static void usage(const char* pro)
{
printf("%s [local_ip][local_port]\n", pro);
}
int startup(const char* _ip, int _port)
{
// 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(3);
}
// 绑定IP、端口号:单独创建出来的套接字只有文件方面的特性,并没有IP和端口号的内容,所以必须手动绑定
struct sockaddr_in local;
local.sin_family = AF_INET;
//主机字节序和网络字节序的转换
local.sin_port = htons(_port);
//字符串和in_addr的转换
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(4);
}
// 设置为监听状态
if(listen(sock, 10) < 0){
perror("listen");
}
return sock;
}
//在运行服务器的时候,我们按照 ./tcp_server IP 端口号的形式,按照命令行参数来满足需求
int main(int argc, char* argv[])
{
//如果参数不符合,我们打印一个手册
if(argc != 3)
{
usage(argv[0]);
return 1;
}
while(1){
//因为argv中全是字符串,而端口号需要的是一个整数,所以用atoi函数转换
int listen_sock = startup(argv[1], atoi(argv[2]));
//网络线路上使用的均是泛型的sockaddr,在linux下sockaddr_in结构是对其的一种具体化,实际上只需要对其进行强转类型就可以
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
if(new_sock < 0){
perror("new_sock");
exit(5);
}
printf("a new client: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//服务器端读取数据显示,并回复相同内容
while(1){
ssize_t s = read(new_sock, buf, sizeof(buf)-1);
if(s < 0){
perror("read");
exit(6);
} else if(s == 0){
printf("client quit..\n");
close(new_sock);
break;
} else{
buf[s] = 0;
printf("client -> server: %s\n", buf);
s = write(new_sock, buf, strlen(buf));
}
}
}
return 0;
}
tcp_client(客户端)
- 创建客户端:
1:创建套接字
2:connect操作
3:读写操作
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
///
static void usage(const char* pro)
{
printf("%s [local_ip][local_port]\n", pro);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(3);
}
struct sockaddr_in server;
socklen_t len = sizeof(server);
//主机字节序 -> 网络字节序
server.sin_port = htons(atoi(argv[2]));
//字符串 -> in_addr
server.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sock, (struct sockaddr*)&server, len);
if(ret < 0){
perror("connect");
exit(4);
}
char buf[1024];
while(1){
printf("please inter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s < 0){
perror("read");
break;
}else if(s > 0){
buf[s-1] = 0;
write(sock, buf, strlen(buf));
read(sock, buf, sizeof(buf)-1);
printf("server->client: %s\n",buf);
}
}
return 0;
}
总结: 对于这样的服务器,一次只能服务一个客户端并不能满足客户端的基本需求,所以我们要对于这样的服务器进行优化
服务器优化方案一:多进程
- 优化方案
1:让服务器创建子进程来代替服务器进行服务
2:为了不阻塞父进程,我们将子进程再fork一次,退出子进程,让子进程的子进程(以下称孙进程)去完成需求,由于孙进程的父进程已经退出所以成为孤儿进程
3:在创建子程序时,关闭文件描述符
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
///
static void usage(const char* pro)
{
printf("%s [local_ip][local_port]\n", pro);
}
int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(3);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(4);
}
if(listen(sock, 10) < 0){
perror("listen");
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
while(1){
int listen_sock = startup(argv[1], atoi(argv[2]));
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);
if(new_sock < 0){
perror("new_sock");
exit(5);
}
printf("a new client: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
//创建了一个子进程代替我们处理客户端的请求---与上述单进程的不同之处 pid_t id = fork();
if(id<0){
perror("fork");
exit(6);
}else if(id==0){//child
close(listen_sock);//关闭不必要的文件
if(fork > 0){//两次fork
exit(7);
}
char buf[1024];
while(1){
int r= read(new_sock);
if(r<0){
perror("read");
close(new_sock);
break;
}else if(r==0){
close(new_sock);
printf("clinet quit...\n");
break;
}
buf[r] = 0;
printf("clien: %S\n",buf);
write(new_sock,buf,strlen(buf));
close(new_sock);
}
}else{//father
close(new_sock);//父进程继续负责监听客户请求,必须关闭new_sock文件 }
}
服务器优化方案二:多线程
常见错误分析
bind 普遍遭遇的问题是试图绑定一个已经在使用的端口。该陷阱是也许没有活动的套接字存在,但仍然禁止绑定端口(bind 返回 EADDRINUSE),它由 TCP 套接字状态 TIME_WAIT 引起。该状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。 等待 TIME_WAIT 结束可能是令人恼火的一件事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。 考虑清单 3 的例子。在绑定地址之前,我以 SO_REUSEADDR 选项调用 setsockopt。为了允许地址重用,我设置整型参数(on)为 1 (不然,可以设为 0 来禁止地址重用)