1.在TCP/IP协议中,IP地址+TCP/UCP端口号 —-》唯一标识网络通讯中的一个进程,
并且IP地址+端口号被称为socket。
2.内存中的多字节数据相对于内存地址有大端和小端之分,
而网络数据流同样有大端和小端之分。
TCP/IP协议规定,网络数据流应采用大端字节序,即就是低地址高字节。
所以:发送端把数据发送到发送缓冲区之前需要做字节序的转换。
而接收主机在接收时也需要将接收缓冲区中的数据做字节序的转换。
将网络字节序和主机字节序相互转换的函数:
3.socket地址的数据类型及相关函数:
socket API是一层抽象的网络编程接口,使用于各种底层网络协议(IPV4,IPV6)
也就是该接口使得任何的网络协议都可以使用同一个接口,而参数不匹配等问题都是该函数要做的工作。
sockaddr的数据结构:
因为要使用统一的接口,所以在传参时需要注意将所有类型的结构体指针都强转为(struct sockaddr*)。
可能有朋友觉得为什么不用void*呢?这样还会使得接口更加的简单,而且使每次的传参过程简单好多,不需要了解结构体的类型,直接传就可以,因为void*
可以接收任意类型的指针。
其实也很简单,因为sock API的实现早于ANSI C标准化,那时还没有void*类型,y8inwie,这些函数的参数用struct sockaddr*
类型表示,在传递参数之前要强制类型转换一下。
4.对于IP地址,存放在结构体sockaddr_in中的成员struct in_addr sin_addr
表示32位的IP地址。
而我们通常习惯性的用点分十进制的字符串表示IP地址。
下面函数就可以完成我们的需求—-》将点分十进制的IP字符串转换为整形。
1)字符串转in_addr的函数:
①创建一个套接字(获得一个文件描述符)
对于server和client来说,都需要创建一个套接字,来标识自己主机的IP地址及端口号。
一对套接字代表一个连接。
type:SOCK_STREAM表示tcp协议是面向字节流的传输协议。
SOCK_DGRAM表示udp协议是面向数据报的传输协议。
socket()如果调用成功的话,就返回一个文件描述符,应用程序可以用read/write来读写文件。
如果失败,就返回-1。
②bind(绑定本地主机的IP地址和端口号)
服务器程序所监听的网络地址和端口号一般通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接。
因此服务器需要调用调用bind()绑定一个固定的网络地址和端口号。
由于客户端不需要固定的端口号,因此不需要绑定,。
客户端的端口号是由内核自动分配的。
功能:
将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
返回值:
成功返回0,失败返回-1;
注意:
这里的网络地址可以是INADDR_ANY (0),这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
③listen(监听)—-》只有服务器端才会调用的接口函数。
客户端不需要监听,只需要请求连接就好。
连接等待状态,如果接收到更多的连接请求就忽略。
返回值:成功返回0,失败返回-1.
④accept–>(接收请求)
描述信息:
三方握手完成后(tcp底层所做的工作),服务器调用accept()阻塞式等待接收请求,直到有客户端连接上来。
成功返回时accept函数带回了客户端的IP地址和端口号,因为tcp是全双工的,所以服务器端也可能会发送消息给客户端,那么就需要知道客户端的套接字,即就是客户端的IP地址和端口号。
⑤connect—-》(客户端需要的接口函数)
客户端需要调用该函数连接服务器,connect和bind的参数形式一致,区别在于绑定时绑定自己的地址,而连接时需要对方的地址。
返回值:成功返回0,失败返回-1.
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]\n",proc);
}
int startup(char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
socklen_t len = sizeof(local);
if(bind(sock,(struct sockaddr*)&local,len)< 0)
{
perror("bind");
exit(2);
}
if(listen(sock,3) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(4);
}
//接待客人的人
int listen_sock = startup(argv[1],atoi(argv[2]));
printf("sock: %d\n",listen_sock);
char buf[1024];
while(1)
{
//接收client套接字的信息
struct sockaddr_in client;
socklen_t len = sizeof(client);
//newsock == 服务人员
int newsock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(newsock < 0)
{
perror("accept");
continue;//继续接听下一个
}
//将网络中的数据转换为主机用户可以看懂的数据
printf("get new client [%s: %d]\n",\
inet_ntoa(client.sin_addr.s_addr,\
ntohs(client.sin_port));
//1.read 2.write
while(1)
{
int s = read(newsock,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = '\0';
printf("client# %s\n",buf);
write(newsock,buf,strlen(buf));//服务器将读到的信息给客户端回显回去
}else if(s == 0)
{
printf("client quit\n");
break;
}
else{
break;
}
}
close(newsock);
}
close(listen_sock);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]",proc);
}
//./client server_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
//1.创建socket
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
return 1;
}
//2.connect
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);//将点分式的字符串转换为能在网络上传输的
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
char buf[1024];
//先写后读
while(1)
{
printf("please Enter# ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf) - 1);
if(_s > 0)
{
buf[_s] = 0;
printf("server echo# %s\n",buf);
}
}
}
close(sock);
return 0;
}
从上面可以看出,我们编写的服务器端程序只能服务一个客户端。。。
但是一般来说,正常的服务器都可以服务服务多个客户的吧。
所以,下面我们来编写多进程和多线程的服务器。
//server
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]\n",proc);
}
int startup(char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
socklen_t len = sizeof(local);
if(bind(sock,(struct sockaddr*)&local,len)< 0)
{
perror("bind");
exit(2);
}
if(listen(sock,3) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(4);
}
//接待客人的人
int listen_sock = startup(argv[1],atoi(argv[2]));
printf("sock: %d\n",listen_sock);
//需要让子进程的子进程去提供服务
//父进程继续监听
char buf[1024];
while(1)
{
//接收client套接字的信息
struct sockaddr_in client;
socklen_t len = sizeof(client);
//newsock == 服务人员
int newsock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(newsock < 0)
{
perror("accept");
continue;//继续接听下一个
}
//将网络中的数据转换为主机用户可以看懂的数据
printf("get new client [%s: %d]\n",\
inet_ntoa(client.sin_addr.s_addr,\
ntohs(client.sin_port)));
pid_t id = fork();
if(id < 0)
{
perror("fork");
close(newsock);
return 1;
}
else if(id == 0)
{
//因为子进程会继承父进程的文件描述符表,而子进程只需要newsock(提供服务的套接字)
//child
close(listen_sock);//子进程关闭监听套接字
if(fork() > 0)
{
//child ---->father
exit(0);//子进程充当父进程的角色,父进程退出后,会导致子进程成为孤儿进程
}
//child->child-->也就是孙子进程(但是注意linux中只有父子关系,没有所谓的爷孙关系)
//让孙子进程去服务--?》读和写
while(1)
{
int s = read(newsock,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = '\0';
printf("client# %s\n",buf);
write(newsock,buf,strlen(buf));//服务器将读到的信息给客户端回显回去
}else if(s == 0)
{
printf("client quit\n");
break;
}
else{
break;
}
}
close(newsock);
exit(1);//当子进程执行完之后需要退出
}
else{
//father
close(newsock);//父进程只负责监听
waitpid(id,NULL,0);
}
//1.read 2.write
}
close(listen_sock);
return 0;
}
//client
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]",proc);
}
//./client server_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
//1.创建socket
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
return 1;
}
//2.connect
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);//将点分式的字符串转换为能在网络上传输的
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
char buf[1024];
//先写后读
while(1)
{
printf("please Enter# ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf) - 1);
if(_s > 0)
{
buf[_s] = 0;
printf("server echo# %s\n",buf);
}
}
}
close(sock);
return 0;
}
//server
#include
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]\n",proc);
}
int startup(char* ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
socklen_t len = sizeof(local);
if(bind(sock,(struct sockaddr*)&local,len)< 0)
{
perror("bind");
exit(2);
}
if(listen(sock,3) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
void* handle(void* argc)
{
int newsock = (int)argc;
char buf[1024];
while(1)
{
int s = read(newsock,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = '\0';
printf("client# %s\n",buf);
write(newsock,buf,strlen(buf));//服务器将读到的信息给客户端回显回去
}else if(s == 0)
{
printf("client quit\n");
break;
}
else{
break;
}
}
close(newsock);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(4);
}
//接待客人的人
int listen_sock = startup(argv[1],atoi(argv[2]));
printf("sock: %d\n",listen_sock);
//需要让子进程的子进程去提供服务
//父进程继续监听
char buf[1024];
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
//newsock == 服务人员
int newsock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(newsock < 0)
{
perror("accept");
continue;//继续接听下一个
}
//将网络中的数据转换为主机用户可以看懂的数据
printf("get new client [%s: %d]\n",\
inet_ntoa(client.sin_addr.s_addr,\
ntohs(client.sin_port)));
//创建一个新线程--》去服务
//主线程依然只是做监听工作
pthread_t tid;
pthread_create(&tid,NULL,handle,(void*)newsock);//将要服务的套接字传参
//线程一般必须显示回收
//pthread_join---->为什么不是回收线程而是分离开线程
//线程分离--》则线程的回收最后仅与操作系统有关
}
close(listen_sock);
return 0;
}
//client
#include
#include
#include
#include
#include
#include
#include
static void Usage(const char* proc)
{
printf("%s : [server_ip][server_port]",proc);
}
//./client server_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
//1.创建socket
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("sock");
return 1;
}
//2.connect
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);//将点分式的字符串转换为能在网络上传输的
if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 2;
}
char buf[1024];
//先写后读
while(1)
{
printf("please Enter# ");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;
write(sock,buf,strlen(buf));
ssize_t _s = read(sock,buf,sizeof(buf) - 1);
if(_s > 0)
{
buf[_s] = 0;
printf("server echo# %s\n",buf);
}
}
}
close(sock);
return 0;
}
Makefile(生成两个可执行程序)
all:client server
.PHONY:clean
client:client.c
gcc -o $@ $^ -g
server:server.c
gcc -o $@ $^ -g
%.o:%.c
clean:
rm -f client server
但是上面的程序还是存在问题,当服务器端和客户端进行一段时间的通信后,服务器端先退出,客户端没有退出,如果这时需要立即启动服务器端时,会发现出现错误:
现在我们一起来聊一下这是什么导致的?要知道服务器如果挂机,一般必须立即重启。
TCP协议规定:
主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL的时间后才能回到CLOSED状态。
现在我们解析下上面的案例:
由于我们先终止了server,所以server是主动关闭连接的一方,所以server就处于TIME_WAIT状态。所以在TIME_WAIT
期间仍然不能再次
监听同样的server端口。
MSL在RFC1122中规定为两分钟,但是各种不同操作系统的实现不同,在Linux上一般经过半分钟后,就可以再次启动server了。
解决方案:
在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为TCP连接没有完全断开指的是newsock没有完全断开,而我们重新监听的是listen_sock,虽然是占用同一个端口,但是IP地址不同,newsock对应的是与某个客户端通讯的一个具体的IP地址。而listen_scok对应的是某个匹配的IP地址。
解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同而IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码
int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
现在我们来检测下改过之后的结果: