一小时学会网络编程

网络通讯

数据传输三要素:
源、目的、长度
服务器:
被动的相应请求。
客户端:
主动的发起请求。
UDP和TCP:
UDP传输,如传输视频,某一帧数据丢失关系也不大,是无连接的传输;但是TCP主要是传输文件或者控制命令这些每一帧都很重要的信息,是可靠的有连接的传输。

TCP

文件读写:
fd=open(“文件名”);
read(fd,buf,len);
write(fd,buf,len);

服务器:
socket();
为通信创建一个节点并返回一个描述符。
bind()
把fd和ip端口绑定建立联系。
listen()
启动监测数据。
accept();
接受一条连接。等待connect()。
send发送数据,recv接收数据。

客户端:
socket()
客户端同样需要建立一个通信节点。
connect()
申请和服务器端建立连接。
建立连接后,send发送数据,recv接收数据。
具体连接过程可以参考TCP三次握手过程。

Linux系统是通过提供套接字(socket)来进行网络编程的.网络程序通过socket和其它几个函数的调用,
会返回一个 通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处.
我们可以通过向描述符读写操作实现网络之间的数据交流.

(一) socket

int socket(int domain, int type,int protocol)

domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等).

  • AF_UNIX只能够用于单一的Unix 系统进程间通信,
  • AF_INET是针对Internet的,因而可以允许在远程主机之间通信(当我们 man socket时发现 domain可选项是 PF_而不是AF_,因为glibc是posix的实现所以用PF代替了AF,不过我们都可以使用的)。

type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等)

  • SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流.
  • SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.

protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了

socket为网络通讯做基本的准备.成功时返回文件描述符,失败时返回-1,看errno可知道出错的详细情况.

(二) bind
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

sockfd:是由socket调用返回的文件描述符.

addrlen:是sockaddr结构的长度.

my_addr:是一个指向sockaddr的指针. 在中有 sockaddr的定义

struct sockaddr{
		unisgned short  as_family;
		char            sa_data[14];
};

不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义

struct sockaddr_in{
	unsigned short          sin_family;    
	unsigned short int      sin_port;
	struct in_addr          sin_addr;
	unsigned char           sin_zero[8];
}

我们主要使用Internet,所以
sin_family一般为AF_INET,
sin_addr设置为INADDR_ANY表示可以和任何的主机通信,(设置的是其成员sin_addr.s_addr)
sin_port是我们要监听的端口号.
sin_zero[8]是用来填充的.
需要注意的是:sin_addr、sin_port都需要经过字节序转换,将主机的字节序转化成网络字节序。接口函数为htons。

bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样

(三) listen
int listen(int sockfd,int backlog)
sockfd:是bind后的文件描述符.

backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以介绍的排队长度.
listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.

(四) accept
int accept(int sockfd, struct sockaddr *addr,int *addrlen)

  • sockfd:是listen后的文件描述符.

  • addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了.

accept成功时,返回最后的客户端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1

bind,listen和accept是服务器端用的函数.accept调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接.

(五) connect
int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)

  • sockfd:socket返回的文件描述符.

  • serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址

  • addrlen:serv_addr的长度

connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.

(六) recv
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数分别代表句柄,用于储存数据的buf,数据的长度len,flags用于特殊设置,在此设为0。返回值是接收数据的长度。

(七) send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数类型与recv一致。

程序设计:
目前最常用的服务器模型.
<一>循环服务器:循环服务器在同一个时刻只可以响应一个客户端的请求
<二>并发服务器:并发服务器在同一个时刻可以响应多个客户端的请求

TCP通讯为了实现并发服务器,常用的方法有两种:1.TCP服务器(建立子进程);2.多路复用IO

TCP服务器(建立子进程)

为了弥补循环TCP服务器的缺陷,人们又想出了并发服务器的模型. 并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是服务器创建一个 子进程来处理.

算法如下:

socket(...);
bind(...);
listen(...);
while(1)
{
      accept(...);
      if(fork(..)==0)
        {
            while(1)
             {       
              read(...);
              process(...);
              write(...);
             }
         close(...);
         exit(...);
        }
      close(...);
} 

TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况. 不过也同时带来了一个不小的问题.为了响应客户机的请求,服务器要创建子进程来处理. 而创建子进程是一种非常消耗资源的操作.

下面代码实现:
首先实现socket的基本配置:
服务器端:

iSeverFd=socket(AF_INET,SOCK_STREAM,0);
if(iSeverFd<0)
{
	printf("socket error!");
	return -1;
}
tiSockaddrSeverIn.sin_family=AF_INET;
tiSockaddrSeverIn.sin_port=htons(SERVERIPPORT);/*自定义一个端口号*/
tiSockaddrSeverIn.sin_addr.s_addr=INADDR_ANY;
memset(tiSockaddrSeverIn.sin_zero,0,8);

iRet=bind(iSeverFd,(struct sockaddr *)&tiSockaddrSeverIn,sizeof(tiSockaddrSeverIn));
if(iRet<0)
{
	printf("bind error!");
	return -1;
}
iRet=listen(iSeverFd,999);
if(iRet<0)
{
	printf("listen error!");
	return -1;
}

尤其要注意的是这句:tiSockaddrSeverIn.sin_addr.s_addr=INADDR_ANY;。写入参数的对象是结构体sin_addr的子成员s_addr。查了一下,这个结构体如下:

struct in_addr {
	__be32	s_addr;
};
struct in_addr	sin_addr;

INADDR_ANY是一个正整数,直接赋值给sin_addr一定会出错。

客户端中:

iClientFd=socket(AF_INET,SOCK_STREAM,0);
if(iClientFd<0)
{
	printf("Client error!");
	return -1;
}

tiSockaddrSeverIn.sin_family= AF_INET;
tiSockaddrSeverIn.sin_port= htons(SERVERIPPORT);/* host to net, short */
if (0==inet_aton(argv[1], &tiSockaddrSeverIn.sin_addr))
{
	printf("invalid server_ip\n");
	return -1;
}
memset(tiSockaddrSeverIn.sin_zero, 0, 8);

客户端的配置只需要一个scoket()函数。
随后客户端调用connect()函数。主动向客户端请求建立连接。

iRet=connect(iClientFd,(struct sockaddr *)&tiSockaddrSeverIn,sizeof(tiSockaddrClientIn));/*循环申请建立连接*/
if(iRet<0)
{
	printf("connect error\n");
	return -1;
}

此时,客户端进程正休眠在accpet()函数位置:

iClientFd=accept(iSeverFd,(struct sockaddr *)&tiSockaddrClientIn,&iRevNum);/*循环等待建立连接*/

如果有服务器有建立连接的请求,相应请求并返回一个句柄,用于随后与客户端通信;为实现与多个客户端建立连接,可以每把accept()从休眠中唤醒一次,就建立一个进程。

实现多个客户端连接一个服务端:

while(1)
{
	iClientFd=accept(iSeverFd,(struct sockaddr *)&tiSockaddrClientIn,&iRevNum);/*循环等待建立连接*/
	if(iClientFd<0)
	{
		printf("accept error!");
		return -1;
	}
	else
	{
		if(!fork())/*创建一个进程用以接收数据*/
		{
			iClientNum++;
			printf("Get connect from client %d : %s\n",iClientNum,inet_ntoa(tiSockaddrClientIn.sin_addr));
			while(1)/*等待接收数据*/
			{
				iRevNumRel=recv(iClientFd,pcRevBuf,iRevNum,0);
				if(iRevNumRel<=0)
				{
					printf("recv error");
					close(iClientFd);
				}
				else
				{	
					pcRevBuf[iRevNumRel]='\0';
					printf("Get connect from client %d : %s\n",iClientNum,pcRevBuf);
					/*收到数据之后在回发一次数据*/
					send(iClientFd,pcRevBuf,iRevNum,0);			
				}
			}
		}
	}	
}

服务端发送数据的代码:

while(1)
{
	if (fgets(pcSendBuf,iSendNum,stdin))
	{
		iSendNumRel=send(iClientFd,pcSendBuf,iSendNum,0);
		if(iSendNumRel<=0)
		{
			printf("send error :%s\n",pcSendBuf);
		}
		else
		{
			iRevNumRel=recv(iClientFd,pcRevBuf,iSendNum,0);
			printf("send :%s\n",pcRevBuf);
		}
	}
}

最后是通信的关闭,TCP通信的关闭也是具有特色的:由于相比于UDP通信其具有双向性,因此为了保证数据传输的完整,通常会选择只关闭一个方向(具体看四次挥手过程),此时可以调用int shutdown(int sockfd,int howto)实现。
针对不同的howto,系统回采取不同的关闭方式。

  • howto=0这个时候系统会关闭读通道.但是可以继续往接字描述符写.
  • howto=1关闭写通道,和上面相反,着时候就只可以读了.
  • howto=2关闭读写通道,和close一样
    在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用shutdown,那么所有的子进程都不能够操作了,这个时候我们只能够使用close来关闭子进程的套接字描述符。
多路复用IO

为了解决创建子进程带来的系统资源消耗,人们又想出了多路复用I/O模型.
首先介绍一个函数select

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *except fds,struct timeval *timeout)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)

一般的来说当我们在向文件读写时,进程有可能出现阻塞,直到一定的条件满足. 比如我们从一个套接字读数据时,可能缓冲区里面没有数据可读 (通信的对方还没有 发送数据过来),这个时候我们的读调用就会等待(阻塞)直到有数据可读.如果我们不希望阻塞,我们的一个选择是用select系统调用. 只要我们设置好select的各个参数,那么当文件可以读写的时候select会"通知"我们 说可以读写了. 如果还没有满足条件,那么这个进程将休眠。

为了设置文件描述符我们要使用几个宏.

  • FD_SET将fd加入到fdset
  • FD_CLR将fd从fdset里面清除
  • FD_ZERO从fdset中清除所有的文件描述符
  • FD_ISSET判断fd是否在fdset集合中

程序框架大概是:
使用select后我们的服务器程序就变成了.

初始话(socket,bind,listen);
while(1)
{
	设置监听读写文件描述符(FD_*);  
	调用select;
	如果是倾听套接字就绪,说明一个新的连接请求建立
	{
		建立连接(accept);
		加入到监听文件描述符中去;
	}
	否则说明是一个已经连接过的描述符
	{
		进行操作(read或者write);
	}
}        

多路复用I/O可以解决资源限制的问题.这模型实际上是将UDP循环模型用在了TCP上面. 这也就带来了一些问题.如由于服务器依次处理客户的请求,所以可能会导致有的客户会等待很久.

UDP

对比TCP模式,客户端和服务端仍然需要首先调用socket()函数获得一个句柄;客户端需要调用bind()函数绑定端口号、IP等信息;客户端则需要connect()函数建立连接。概括而言初始化过程:
客户端:socket->bind
服务端:socket->connect
收发数据方面:

int recvfrom(int sockfd,void *buf,int len,unsigned int flags,
				struct sockaddr * from int *fromlen)
int sendto(int sockfd,const void *msg,int len,unsigned int flags,
				struct sockaddr *to int tolen)

sockfd,buf,len的意义和read,write一样,分别表示套接字描述符,发送或接收的缓冲区及大小.recvfrom负责从 sockfd接收数据,如果from不是NULL,那么在from里面存储了信息来源的情况,如果对信息的来源不感兴趣,
可以将from和fromlen 设置为NULL.sendto负责向to发送信息.此时在to里面存储了收信息方的详细资料.

UDP循环服务器的实现非常简单:UDP服务器每次从套接字上读取一个客户端的请求,处理, 然后将结果返回给客户机.可以用下面的算法来实现.

   socket(...);
   bind(...);
   while(1)
    {
         recvfrom(...);
         process(...);
         sendto(...);
   }

因为UDP是非面向连接的,没有一个客户端可以老是占住服务端. 只要处理过程不是死循环, 服务器对于每一个客户机的请求总是能够满足.
举一个例子:

/*           服务端程序  server.c           */
#define SERVER_PORT     8888
#define MAX_MSG_SIZE    1024

void udps_respon(int sockfd)
{
        struct sockaddr_in addr;
        int    n;
        socklen_t addrlen;
        char    msg[MAX_MSG_SIZE];
       
        while(1)
        {       /* 从网络上读,写到网络上面去   */
                memset(msg, 0, sizeof(msg));
                addrlen = sizeof(struct sockaddr);
                n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,(struct sockaddr*)&addr,&addrlen);
                /* 显示服务端已经收到了信息  */
                fprintf(stdout,"I have received %s",msg);
                sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen);
        }
}

int main(void)
{
        int sockfd;
        struct sockaddr_in      addr;
       
        sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(sockfd<0)
        {
                fprintf(stderr,"Socket Error:%s\n",strerror(errno));
                exit(1);
        }
        bzero(&addr,sizeof(struct sockaddr_in));
        addr.sin_family=AF_INET;
        addr.sin_addr.s_addr=htonl(INADDR_ANY);
        addr.sin_port=htons(SERVER_PORT);
        if(bind(sockfd,(struct sockaddr *)&addr,sizeof(struct sockaddr_in))<0)
        {
                fprintf(stderr,"Bind Error:%s\n",strerror(errno));
                exit(1);
        }
        udps_respon(sockfd);
        close(sockfd);
}

客户端程序

/*          客户端程 序             */
void udpc_requ(int sockfd,const struct sockaddr_in *addr,socklen_t len)
{
        char buffer[MAX_BUF_SIZE];
        int n;
        while(fgets(buffer,MAX_BUF_SIZE,stdin))       
        {        /*   从键盘读入,写到服务端   */
                sendto(sockfd,buffer,strlen(buffer),0,addr,len);
                bzero(buffer,MAX_BUF_SIZE);

                /*   从网络上读,写到屏幕上    */
                                memset(buffer, 0, sizeof(buffer));
                n=recvfrom(sockfd,buffer,MAX_BUF_SIZE, 0, NULL, NULL);
                if(n <= 0)
                                {
                                        fprintf(stderr, "Recv Error %s\n", strerror(errno));
                                        return;
                                }
                                buffer[n]=0;
                fprintf(stderr, "get %s", buffer);
        }
}


int main(int argc,char **argv)
{
        int sockfd,port;
        struct sockaddr_in      addr;
       
        if(argc!=3)
        {
                fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]);
                exit(1);
        }
       
        if((port=atoi(argv[2]))<0)
        {
                fprintf(stderr,"Usage:%s server_ip server_port\n",argv[0]);
                exit(1);
        }
       
        sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(sockfd<0)
        {
                fprintf(stderr,"Socket  Error:%s\n",strerror(errno));
                exit(1);
        }      
        /*      填充服务端的资料      */
        bzero(&addr,sizeof(struct sockaddr_in));
        addr.sin_family=AF_INET;
        addr.sin_port=htons(port);
        if(inet_aton(argv[1],&addr.sin_addr)<0)
        {
                fprintf(stderr,"Ip error:%s\n",strerror(errno));
                exit(1);
        }
                 if(connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) == -1)
                {
                        fprintf(stderr, "connect error %s\n", strerror(errno));
                        exit(1);
                }
        udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in));
        close(sockfd);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值