OSI 模型本身不是网络体系结构的全部内容,它并未确切地描述用于各层的协议和服务,仅提出每一层应该做什么。不过OSI 已经为各层制定了标准,但并不是参考模型的一部分,而作为单独的国际标准公布的。
TCP/IP 是一组用于实现网络互连的通信协议。Internet 网络体系结构以TCP/IP 为核心。基于TCP/IP 的参考模型将协议分成四个层次,它们分别是:网络访问层、网际互联层、传输层(主机到主机)、和应用层。
TCP/IP是一个网络通信模型,以及一整个网络传输协议家族,是网际网络的基础通信架构。因为该协议家族的两个核心协议:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。故此它常被通称为TCP/IP协议族(TCP/IP Protocol Suite),简称TCP/IP。
一. TCP/IP模型 :TCP/IP 是一组用于实现网络互连的通信协议。Internet 网络体系结构以 TCP/IP 为核心。基于TCP/IP 的参考模型将协议分成四个层次,它们分别是:网络访问层、网际互联层、传输层(主机到主机)、和应用层。
二. IP协议 :IP(Internet Protocol)协议是 TCP/IP 的核心协议。IP 协议(Internet Protocol)又称互联网协议,是支持网间互连的数据报协议。它提供网间连接的完善功能, 包括 IP 数据报规定互连网络范围内的 IP 地址格式。
目前的 IP 地址(IPv4:IP 第 4 版本)由 32 个二进制位表示,每 8 位二进制数为一个整数,中间由小数点间隔,如 159.226.41.98,整个 IP 地址空间有 4 组 8 位二进制数,由表示主机所在的网络的地址以及主机在该网络中的标识共同组成,它通常被分为A,B,C,D,E五类,其中商业应用只用到A,B,C三类1。
UDP 协议(用户数据报协议)是建立在 IP 协议基础之上的,用在传输层的协议。UDP 提供了无连接的数据报服务。UDP 和 IP 协议一样,是不可靠的数据报服务。
UDP 提供无连接服务
UDP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、循环接收数据,用函数recvfrom();
5、关闭网络连接;
UDP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置对方的IP地址和端口等属性;
5、发送数据,用函数sendto();
6、关闭网络连接;
在一般的网络编程中,我们一般不会直接去操作TCP/IP协议的底层,而是用过socket套接字进行编程,他相当于一个TCP/IP与应用程序的中间层,通过套接字中提供的接口函数我们可以很快的上手网络开发。
接下来我们来了解一下socket中的一些常用函数:
一. socket()函数:
int socket(int family, int type, int protocol)
二. bind()函数:
int bind(int sockfd,const struct sockaddr* myaddr,socklen_t addrlen)
三. listen()函数:
int listen(int sockfd, int backlog)
四. accept()函数:
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
五. connect()函数:
int connect(int sockfd,conststruct sockaddr *addr, socklen_t addrlen)
六. send()函数:
int send( SOCKET s, const char FAR *buf, int len, int flags );
七. recv ()函数:
int recv( SOCKET s, char FAR *buf, int len, int flags);
八. sendto ()函数:
int sendto( SOCKET s, const char FAR* buf, int len, int flags, const struct sockaddr FAR* to, int tolen)
九. recvfrom ()函数:
int recvfrom( SOCKET s, char FAR* buf, int len, int flags)
利用socket编程基础实现一个基础的聊天室功能,对新成员的加入进行广播,同时具有群发和私聊两种聊天模式。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MYPORT 8887
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
void echo_ser(int sock)
{
}
int main(void)
{
int sock,portl,numb;
char addrl[2048];
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket error");
struct sockaddr_in servaddr,peeraddr,peeaddr;
memset(&servaddr, 0, sizeof(servaddr));//初始化清空数组
memset(&peeaddr, 0, sizeof(peeaddr));
char ipstr[128];
/*初始化套接字结构体*/
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MYPORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
peeaddr.sin_family = AF_INET;
printf("服务器启动\n",MYPORT);
if (bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind error");
char recvbuf[2048] = {0};
char addr[30][100];//用户ip列表
int port[30];//端口号数组
char name[30][100];//用户名数组
char *ip;
socklen_t peerlen;
int n,nu=0,lon,in=0,xx,check,chec;
while (1)
{
peerlen = sizeof(peeraddr);
memset(recvbuf, 0, sizeof(recvbuf));
n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0,(struct sockaddr *)&peeraddr, &peerlen);46.//通篇程序唯一的接收函数。所有的刷新数据全部来自这里
if(n!=0){
printf("%s %d %s\n",ip=inet_ntop(AF_INET,(struct sockaddr *)&peeraddr.sin_addr.s_addr,ipstr,sizeof(ipstr)),portl=ntohs(peeraddr.sin_port),recvbuf);
/*下面这部分用来做新用户进入控制,如果用户IP地址已经存在,
说明此用户是中途退出后再进入的,就不在对用户数据进行添加,仅仅修改端口号就可以;
如果ip地址是新的IP,那么创建新的数据列。存储新的连接信息。*/
if (recvbuf[0]=='>'){
if(nu==0) in=1;
else{
for(check=0;check<nu;check++){ //检测IP是否重复
in=0;
for(int mm=0;mm<13;mm++){
if(ip[mm]!=addr[check][mm])
{
in=1;
}
}
if(in==0)break; //如果IP地址重复,立即跳出循环,此时的check的值就是重复的IP数组号
}
}
//printf("in %d\n",in);
if(in==1){
for(lon=0;lon<peerlen+1;lon++){
addr[nu][lon]=ip[lon];
name[nu][lon]=recvbuf[lon];
}
port[nu]=portl;
// name[nu]=recvbuf;
xx=nu;
nu++;
}
else{
port[check]=portl;
xx=check;
}
printf("%s进入房间,IP:%s\n",name[xx],addr[xx]);
for(int fnu=0;fnu<nu;fnu++){//向所有在线用户发送新用户进入的信息
peeaddr.sin_addr.s_addr = inet_addr(addr[fnu]);
peeaddr.sin_port = htons(port[fnu]);
printf("发送欢迎信息给: %s %d\n",addr[fnu],port[fnu]);
sendto(sock, recvbuf, 20, 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
}
memset(recvbuf, 0, sizeof(recvbuf));
}
else if(n > 0)
{//如果接受的数据前面不包含>这说明不是新用户入列信息.
printf("接收到的数据:%s\n",recvbuf);
/*这一部分代码针对私聊信息进行处理,若果接受的信息包含@字符,这说明这是私聊*/
if(recvbuf[0]=='@')
{
int mn;
for(mn=0;mn<100;mn++){//获取私聊对象用户名
if(recvbuf[mn]=='^') break;
}
for(numb=0;numb<nu;numb++)
{//遍历在线的用户名数组,如果匹配成功,就可以获得这个用户的数组号,从而得到他的全部连接信息
int ig=1;
for(int ii=1;ii<mn;ii++){
if(recvbuf[ii]!=name[numb][ii]){
ig=0;
break;
}
}
if(ig==1) break;
}
/*匹配成功,配置接收方socke信息*/
peeaddr.sin_addr.s_addr = inet_addr(addr[numb]);
peeaddr.sin_port = htons(port[numb]);
for(chec=0;chec<nu;chec++){
in=0;
for(int mm=0;mm<13;mm++){
if(ip[mm]!=addr[chec][mm])
{
in=1;
}
}
if(in==0)break;
}
printf("mn:%d chec %d\n",mn,chec);
memset(addrl, 0, sizeof(addrl));
for(int ad=0;ad<mn;ad++)
{
addrl[ad]=name[chec][ad+1];
}
strcat (addrl,":");
printf("%s发送信息给:%s\n",addrl,addr[numb]);
strcat (addrl,recvbuf);
sendto(sock, addrl, strlen(addrl), 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
}
else{
/*对于群发消息,这里开始循环发送信息给所有用户*/
for(chec=0;chec<nu;chec++){
in=0;
for(int mm=0;mm<13;mm++){
printf("%c %c\n",ip[mm],addr[chec][mm]);
if(ip[mm]!=addr[chec][mm])
{
in=1;
}
}
printf("%s \n",addr[chec]);
if(in==0)break;
}
for(int tt=0;tt<nu;tt++){
printf("%s \n",addr[tt]);
}
for(int ad=0;ad<strlen(name[chec]);ad++)
{
addrl[ad]=name[chec][ad+1];
}
strcat (addrl,":");
printf(" %s发送信息: %s\n",addrl,recvbuf);
strcat (addrl,recvbuf);
for(int fnu=0;fnu<nu;fnu++){
printf("发送信息给:%s\n",addr[fnu]);
peeaddr.sin_addr.s_addr = inet_addr(addr[fnu]);
peeaddr.sin_port = htons(port[fnu]);
sendto(sock, addrl, strlen(addrl), 0,(struct sockaddr *)&peeaddr, sizeof(peeaddr));
printf(" %s\n",addrl);
}
memset(addrl, 0, sizeof(addrl));
}
}
}
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sock);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX 10
#define MYPORT 8887
#define user ">winds"
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
char* SERVERIP = "192.168.1.100";
pthread_t thread[2]; //两个线程
pthread_mutex_t mut;
int number=0;
int i;
pthread_t id_1;
int sock;
struct sockaddr_in servaddr;
pid_t pid;
int ret;
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
int flag=1;
char addr[30]={0};
//线程一,这个线程负责循环接收信息并且显示出来。
void *thread1()
{
while (flag)
{
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
if(recvbuf[0]=='>'){
printf("欢迎:%s 进入房间\n",recvbuf);
}
else if(recvbuf[0]!='>'){
printf("%s\n",recvbuf);
}
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sock);
pthread_exit(NULL);
}
//线程二,这个线程用来阻塞接收输入信息,并发送出去
void *thread2()
{
// printf("thread2 : I'm thread 2/n");
while (flag)
{
printf("发送:%s\n",sendbuf);
fgets(sendbuf, sizeof(sendbuf), stdin);
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
close(sock);
pthread_exit(NULL);
}
void thread_create(void) //创建两个线程
{
int temp;
memset(&thread, 0, sizeof(thread));
/*创建线程*/
if((temp = pthread_create(&thread[0], NULL, thread1, NULL)) != 0)
printf("线程1创建失败!/n");
//else
//printf("线程1被创建/n");
if((temp = pthread_create(&thread[1], NULL, thread2, NULL)) != 0)
printf("线程2创建失败");
//else
// printf("线程2被创建/n");
}
void thread_wait(void)
{
/*等待线程结束*/
if(thread[0] !=0)
{ //comment4
pthread_join(thread[0],NULL);
printf("线程1已经结束/n");
}
if(thread[1] !=0)
{
//comment5
pthread_join(thread[1],NULL);
printf("线程2已经结束/n");
}
}
int main()
{
/*用默认属性初始化互斥锁*/
pthread_mutex_init(&mut,NULL);
printf("用户: %s\n",user);
if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
ERR_EXIT("socket");
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(MYPORT);
servaddr.sin_addr.s_addr = inet_addr(SERVERIP);
sendto(sock,user,8,0,(struct sockaddr *)&servaddr, sizeof(servaddr)); //发送用户信息给服务器
thread_create();
thread_wait();
return 0;
}
这是用户进入房间时,服务器的显示情况,每一次一个新用户进入房间,服务器将会把该用户的用户名向所有在线用户发送一遍,如果该用户重复进入服务器只更新该用户的端口号,并且不再修改IP信息,也不会再添加新的用户信息组。
在用户发送消息时服务器端的提示内容:
手机端安装"手机CAPP"
app即可运行客户端程序:
解决
:在进行服务器编程的时候,我使用了几个用指针方式开辟的数组变量,因此他们的长度并不确定。这里改变他的定义方式,限制他们的大小。或者将他们定义的顺序改变一下,都可以解决这个问题。 while (flag)
{
ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
pid = fork();
if(pid == 0){
if(recvbuf[0]=='>'){
printf("欢迎:%s 进入房间\n",recvbuf);
}
}
else{
if(recvbuf[0]!='>'){
printf("%s\n",recvbuf);
}
printf("发送:%s\n",sendbuf);
fgets(sendbuf, sizeof(sendbuf), stdin);
sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
}
memset(recvbuf, 0, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}
这样一来程序可以正常执行,并且开始运行全部正常,但是再运行了几十条消息之后系统响应速度明显下降,最终只能强制关机。
原因:在函数fork()之后立即开始两个进程。分别执行fork=0子进程和fork=pid(子进程号)父进程。子进程会把整个程序资源和代码都复制一份,在另一个存储空间执行,它的资源将不再和父进程共享。
在我的这段代码中,在while语句里面创建了子程序,并且在子程序代码中没有while结束标志。这样一来子程序会一直在while创建子程序的子程序。每发送一条信息,或者每接收一条信息,都会创建一个子程序,以此维持对信息的不间断接收和对输入的阻塞等待。
但是!这样的程序是具有极大地弊端的。因为我们的子进程不会结束,并且不断创建新的子进程,当子进程创建下一个子进程后,上一个子进程其实已经没有作用了,但是他还在作为父进程维持当前的子进程,所以我们不能在父进程里面回收他的资源,但是如果加入子进程结束条件,则会导致无法持续监测接收数据。这就形成了一个两难之选:在这里我选择了牺牲系统资源。这样一来系统会出现非常多的僵尸进程。我们知道在系统中能够分配的进程号是有限的,一旦僵尸进程占用大量的进程号就会导致系统错乱,甚至崩溃。
比如著名的while-fork炸弹:
int main()
{
while(1)
fork();
return 0;
}
你会发现这与我的代码结构十分相似,只不过这个语句是瞬间创造大量子进程崩溃你的系统,而我的代码是慢性崩溃你的系统。
解决
:采用多线程方式实现客户端的代码设计;
关于IP地址的更多信息 ↩︎
这部分来自这位博主的博客 ↩︎