首页 > 代码库 > Linux网络编程一站式学习

Linux网络编程一站式学习

提要

      学过很多遍计算机网络,依然不会网络编程。

      看完这篇文章之后就不会是这样了。

       环境:Ubuntu14.04 64bit


何为Socket

是基于TCP/IP的网络应用编程中使用的有关数据通信的概念,通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。


基于TCP协议的网络程序

下图是基于TCP协议的客户端/服务器程序的一般流程:



借助这个图,我们可以很清晰地把socket的一些接口和TCP/IP中服务器和客户端状态转换的对应关系。

准备状态

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。

数据传输的过程

建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。


码砖开始

先跑代码再解释。

服务端,server.c

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;

	listenfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
    
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	listen(listenfd, 20);

	printf("Accepting connections ...\n");
	while (1) {
		cliaddr_len = sizeof(cliaddr);
		connfd = accept(listenfd, 
				(struct sockaddr *)&cliaddr, &cliaddr_len);
	  
		n = read(connfd, buf, MAXLINE);
		printf("received from %s at PORT %d\n",
		       inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
		       ntohs(cliaddr.sin_port));
    
		for (i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		write(connfd, buf, n);
		close(connfd);
	}
}


客户端,client.c

/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;
	char *str;
    
	if (argc != 2) {
		fputs("usage: ./client message\n", stderr);
		exit(1);
	}
	str = argv[1];
    
	sockfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);
    
	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	write(sockfd, str, strlen(str));

	n = read(sockfd, buf, MAXLINE);
	printf("Response from server:\n");
	write(STDOUT_FILENO, buf, n);

	close(sockfd);
	return 0;
}

编译

gcc client.c -o client
gcc server.c -o server

运行结果



从头文件开始解释

unistd.h - unistd.h 中所定义的接口通常都是大量针对系统调用的封装(英语:wrapper functions),如 fork、pipe 以及各种 I/O 原语(read、write、close 等等)。 
 sys/socket.h - 提供 socket 函数及数据结构 .
netinet/in.h - 定义数据结构sockaddr_in,包含IPv4和IPv6的地址格式定义。
arpa/inet.h - 提供IP地址转换函数,比如大小端的转换,还有ip地址的表达方式的转换。


看server的main函数。

16行,声明两个socketaddr_in结构体。IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址.IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。

17行,声明一个记录客户端地址长度的变量,socklen_t就是socket中用来表示长度的类型。Linus Torvalds(他希望有更多的人,但显然不是很多) 努力向他们解释使用size_t是完全错误的,因为在64位结构中 size_t和int的长度是不一样的,而这个参数(也就是accept函数 的第三参数)的长度必须和int一致,因为这是BSD套接字接口 标准.最终POSIX的那帮家伙找到了解决的办法,那就是创造了 一个新的类型"socklen_t". 

23行,打开一个网络通信端口,函数原型是:

int socket(int family, int type, int protocol);

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。


25-28行,首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为8000。


30行,调用bind绑定一个固定的网络地址和端口号。bind()成功返回0,失败返回-1。函数原型:

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。


32行,声明listenfd处于监听状态,并且最多允许有20个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。


25-49行,整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。


客户端少了bind,多了个connect函数。由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。connect函数原型:

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。


小小的封装一下

代码虽然可以work,但是看着着实有点蛋疼,而且整个代码有流程的报错,出错了也很难处理,首先是把socket的相关函数封装一遍,

wrap.h

#include <stdlib.h>
#include <sys/socket.h>
#include <errno.h>

//Print error message and exit.
void perr_exit(const char *s);

//Server accept message from client.
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);

void Bind(int fd, const struct  sockaddr *sa, socklen_t salen);

void Connect(int fd, const struct  sockaddr *sa, socklen_t salen);

void Listen(int fd, int backlog);

int Socket(int family, int type, int protocol);

ssize_t Read(int fd, void *ptr, size_t nbytes);

ssize_t Readn(int fd, void *vptr, size_t n);

ssize_t Write(int fd, const void *ptr, size_t nbytes);

ssize_t Writen(int fd, const void *vptr, size_t n);

static ssize_t my_read(int fd, char *ptr);

ssize_t Readline(int fd, void *vptr, size_t maxlen);

void Close(int fd);

wrap.c

#include "wrap.h"

//Print error message and exit
void perr_exit(const char *s)
{
	perror(s);
	exit(1);
}

//Server accept message from client.
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int n;

again:
	if((n = accept(fd, sa, salenptr)) < 0)
	{
		if((errno == ECONNABORTED) || (errno == EINTR))
			goto again;
		else
			perr_exit("accept error");
	}
	return n;
}

void Bind(int fd, const struct  sockaddr *sa, socklen_t salen)
{
	if(bind(fd, sa, salen) < 0)
	{
		perr_exit("bind error");
	}
}

void Connect(int fd, const struct  sockaddr *sa, socklen_t salen)
{
	if(connect(fd, sa, salen) < 0)
	{
		perr_exit("connet error");
	}
}

void Listen(int fd, int backlog)
{
	if(listen(fd, backlog) < 0)
	{
		perr_exit("listen error");
	}
}

int Socket(int family, int type, int protocol)
{
	int n;
	if((n = socket(family, type, protocol)) < 0)
		perr_exit("socket error");
	return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;
again:
	if((n = read(fd, ptr, nbytes)) == -1)
	{
		if(errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

ssize_t Readn(int fd, void *vptr, size_t n)
{
	size_t  nleft;
	ssize_t nread;
	char   *ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;
			else
				return -1;
		} else if (nread == 0)
			break;

		nleft -= nread;
		ptr += nread;
	}
	return n - nleft;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
	ssize_t n;
again:
	if((n = write(fd, ptr, nbytes)) == -1)
	{
		if(errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

ssize_t Writen(int fd, const void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char *ptr;
	ptr = vptr;
	nleft = n;
	while(nleft > 0)
	{
		if((nwritten = write(fd, ptr, nleft)) <= 0)
		{
			if(nwritten < 0 && errno == EINTR)
			{
				nwritten = 0;
			}
			else
			{
				return -1;
			}
		}
		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

static ssize_t my_read(int fd, char *ptr)
{
	static int read_cnt;
	static char *read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0) {
	again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return -1;
		} else if (read_cnt == 0)
			return 0;
		read_ptr = read_buf;
	}
	read_cnt--;
	*ptr = *read_ptr++;
	return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t n, rc;
	char    c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c  == ‘\n‘)
				break;
		} else if (rc == 0) {
			*ptr = 0;
			return n - 1;
		} else
			return -1;
	}
	*ptr  = 0;
	return n;
}

void Close(int fd)
{
	if(close(fd) == -1)
	{
		perr_exit("close error");
	}
}

这里的错误处理主要使用了erron和perr函数。

当linux中的C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因,在实际编程中用这一招解决了不少原本看来莫名其妙的问题。但是errno是一个数字,代表的具体含义还要到errno.h中去阅读宏定义,而每次查阅是一件很繁琐的事情。有下面几种方法可以方便的得到错误信息


(1)void perror(const char *s)
函数说明
perror ( )用来将上一个函数发生错误的原因输出到标准错误(stderr),参数s 所指的字符串会先打印出,后面再加上错误原因 字符串。此错误原因依照全局变量
errno 的值来决定要输出的字符串。

(2) char *strerror(int errno)
将错误代码转换为字符串错误信息,可以将该字符串和其它的信息组合输出到用户界面例如
fprintf(stderr,"error in CreateProcess %s, Process ID %d
",strerror(errno),processID)
注:假设processID是一个已经获取了的整形ID


(3)printf("%m", errno);
另外不是所有的地方发生错误的时候都可以通过error获取错误代码,例如下面的代码段


将client改成连接成功后可以多次发送消息的模式。

client.c

/* client.c */

#define MAXLINE 80
#define SERV_PORT 8000

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;
	char *str;
    
	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);
    
	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	while(fgets(buf, MAXLINE, stdin) != NULL)
	{
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, MAXLINE);
		if(n == 0)
		{
			printf("The other side has been closed.\n");
		}
		else
		{
			Write(STDOUT_FILENO, buf, n);
		}
	}

	Close(sockfd);
	return 0;
}


sever.c也要做一点修改,使它可以多次处理同一客户端的请求。

/* server.c */
#define MAXLINE 80
#define SERV_PORT 8000

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
    
	Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	Listen(listenfd, 20);

	printf("Accepting connections ...\n");
	while (1) {
		cliaddr_len = sizeof(cliaddr);
		connfd = Accept(listenfd, 
				(struct sockaddr *)&cliaddr, &cliaddr_len);

		while(1)
		{
			n = Read(connfd, buf, MAXLINE);
			if(n == 0)
			{
				printf("The other side has been closed!\n" );
				break;
			}
			printf("received from %s at PORT %d\n",
		       inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
		       ntohs(cliaddr.sin_port));
    
			for (i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
			Write(connfd, buf, n);
		}
		Close(connfd);
	}
}

运行结果



小作业 - 简单的Web服务器

先按下面的步骤做:

1.终端输入ifconfig,查看自己的ip地址;

2.运行上面编译好的server程序;

3.在浏览器的地址栏里面输入:X.X.X.X:80

(X.X.X.X是你的ip地址)

得到浏览器的运行结果是



这就是一个简单的http请求。


在这个通信过程中,服务器就是我们的server程序,浏览器就是client。server启动之后,就开始监听对应的端口,这时在浏览器中输入本机的ip和端口,这时浏览器向服务器发送的HTTP协议头,server中的处理就是将所有字母都改成大写,然后再write回socket,client最后得到的就是toupper的请求信息。


我们实现的Web服务器只要能正确解析第一行就行了,这是一个GET请求,请求的是服务目录的根目录/(在本例中实际上是/var/www),Web服务器应该把该目录下的索引页(默认是index.html)发给浏览器,也就是把/var/www/index.html发给浏览器。假如该文件的内容如下(HTML文件没必要以"\r\n"换行,以"\n"换行就可以了)。简单的index.html

<html>
<head><title>Test Page</title></head>
<body>
	<p>Hello,Server.</p>
</body>
</html>

则server返回的数据就是

HTTP/1.1 200 OK
Content-Type: text/html

<html>
<head><title>Test Page</title></head>
<body>
	<p>Test OK</p>
</body>
</html>


服务器应答的HTTP头也是每行末尾以回车加换行结束,最后跟一个空行的回车加换行。

HTTP头的第一行是协议版本和应答码,200表示成功,后面的消息OK其实可以随意写,浏览器是不关心的,主要是为了调试时给开发人员看的。虽然网络协议最终是程序与程序之间的对话,但是在开发过程中却是人与程序之间的对话,一个设计透明的网络协议可以提供很多直观的信息给开发人员,因此,很多应用层网络协议,如HTTP、FTP、SMTP、POP3等都是基于文本的协议,为的是透明性(transparency)。

HTTP头的第二行表示即将发送的文件的类型(称为MIME类型),这里是text/html,纯文本文件是text/plain,图片则是image/jpg、image/png等。


然后就发送文件的内容,发送完毕之后主动关闭连接,这样浏览器就知道文件发送完了。这一点比较特殊:通常网络通信都是客户端主动发起连接,主动发起请求,主动关闭连接,服务器只是被动地处理各种情况,而HTTP协议规定服务器主动关闭连接(有些Web服务器可以配置成Keep-Alive的,我们不讨论这种情况)。


最终实现的server代码如下

/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>


#include "wrap.h"

#define MAXLINE 80

void loadConfig(int *port, char *path);

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;
	int pd_index,ret;

	int port;
	char *path = NULL;
	loadConfig(&port, path);

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	int opt=1;
 	setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(port);
    
	Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	Listen(listenfd, 20);

	printf("Accepting connections ...\n");
	while (1) 
	{
		cliaddr_len = sizeof(cliaddr);
		connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

		while(1)
		{
			n = Read(connfd, buf, MAXLINE);
			if(n == 0)
			{
				printf("The other side has been closed!\n" );
				break;
			}
			printf("received from %s at PORT %d\n",
		       inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
		       ntohs(cliaddr.sin_port));

			i=0;
   			char title[15];
   			while(buf[i]!=‘\r‘)
   			{
    			title[i]=buf[i];
    			i++;
   			}
			title[i]=‘\0‘;

   			if(0 == (strcmp(title,"GET / HTTP/1.1")))
   			{
    			bzero(buf,MAXLINE); 
    			strcpy(buf,"HTTP/1.1 200 0K\r\n");
    			write(connfd,buf,strlen(buf));
    			bzero(buf,MAXLINE);
    			strcpy(buf,"Conent_Type: text/html\r\n");
    			write(connfd,buf,strlen(buf));
    			bzero(buf,MAXLINE);
    			buf[0]=‘\r‘;
    			buf[1]=‘\n‘;
    			write(connfd,buf,2);

    			pd_index=open("/var/www/index.html",O_RDONLY);
    			if(pd_index<0)
    			{
    				perr_exit("open file failed.\n");
    			}

    			bzero(buf,MAXLINE);
    			while((ret=read(pd_index,buf,MAXLINE))>0)
    			{
     				write(connfd,buf,ret);
     				printf("send index data...\n");
     				bzero(buf,MAXLINE);
				}
			
			}	
		}
		Close(connfd);
	}
}

void loadConfig(int *port, char *path)
{
	FILE *fd;
	fd = fopen("/etc/myhttpd.conf", "r");
	if(fd < 0)
		perr_exit("Can‘t load config.\n");

	fscanf(fd, "Port=%d\nDirectory=%s",port, path);
}


最终运行结果




参考

Linux C编程一站式学习 - http://c4linux.letaoba.info/

在Ubuntu下实现一个简单的Web服务器 - http://www.linuxidc.com/Linux/2013-10/91160.htm


Linux网络编程一站式学习