现在的位置: 首页 > 综合 > 正文

Socket编程入门—TCP篇

2013年10月01日 ⁄ 综合 ⁄ 共 9645字 ⁄ 字号 评论关闭

 

本文主要介绍socket编程(TCP)的基础知识,通过抽象的概念和具体的实例相结合对socket编程有个基本的了解,本篇介绍TCP,UDP暂且不讲(下篇见)。

本文面向的读者:对网络编程懵懂的IT雏鸟,有一定网络编程经验的IT菜鸟可略过,Orz..

好了,下面进入主题。。。

一、什么是socket(套接字)?

socket的英文原义是“插座”,手机要充电需要用充电器连接到供电插座,看有线电视需要将有线插到提供有线信号的插座,电脑上网也需要将网线插到一个网线插孔(它也可以理解为一个插座)。计算机术语中的socket,套接字,与前面这些“插座”在功能上势必有相类的地方。在同一台主机上,进程之间通信可以是内存共享、发送信号、管道、FIFO等方式,那在网络上不同主机的进程又如何通信呢?!那就是通过socket。

Socket是一个抽象层,不同类型的套接字对应于不同的底层协议族以及协议族内的不同协议栈。这里只讨论TCP/IP协议族的TCP套接字。应用程序可以通过它互相发送/接收数据进行通信。套接字在邦定ip地址和端口后就可以标识一个进程,其它进程就可以利用该套接字的ip地址和端口找到这个进程,通过该套接字进行通信。当然一个进程可以有不同的套接字,一个实例化的套接字只能对应一个进程。通常,不同的端口提供不同的服务,就像开始提到的,不同类型的插座提供不同的功能服务一样。不同主机进程之间的通信,其实也可以说是不同主机套接字之间的通信,套接字就是起到通信桥梁的作用,或者说,套接字是本机上的进程和网络上其它主机的进程进行通信的接口。套接字通信就是从套接字读/写数据。首先,当然得生成一个套接字,用下面的头文件,函数。

#include<sys/types.h>

#include<sys/socket.h>

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

参数:

domain     通信领域,选择用于通信的协议族,常用的以下几个:

                   AF_INET            IPv4协议

                   AF_INET6                   IPv6协议

                   AF_LOCAL         Unix域协议

                   AF_ROUTE        路由套接字

                   AF_KEY              密钥套接字

可能有时会看到像AF_INET为PF_INET,AF_表示地址族,PF_表示协议族,通常它他是等价的,不过一般是用AF_XXX        

         type          指明套接字类型

                            SOCK_STREAM         字节流套接字

                            SOCK_DGRAM                   数据报套接字

                            SOCK_SEQPACKET   有序分组套接字

                            SOCK_RAW                原始套接字

         protocol   指定某个具体的协议类型(可以填0,让内核自动选择)

                            IPPROTO_TCP           TCP传输协议

                            IPPROTO_UDP                   UDP传输协议

                            IPPROTO_SCTP         SCTP传输协议

         返回值     非负整数,与文件描述类似,称它为套接字描述符。

 

说到套接字,不得不说套接字地址,大多数套接字函数都需要一个套接字地址结构的指针作为参数。每个协议都有它自己的套接字地址结构,这里只介绍IPv4,IPv6的套接字地址结构。

(1)       IPv4的套接字地址结构,以sockaddr_in命名,定义在<netinet/in,h>头文件中。

下面给出POSIX的定义,不同系统可能稍微有点不同,但还是大同小异。

struct in_addr {

         in_addr_t                   s_addr;              //32网络字节序的IP地址,一般是uint32_t类型

};

 

struct sockaddr_in {

         uint8_t              sin_len;             // 结构体的长度

         sa_family_t      sin_family;        //AF_INET,任何无符号整型

         in_port_t          sin_port;           //16位网络字节序的端口号,至少16位的无符号整型

         struct in_addr sin_addr;          //32位网络字节序的IP地址

         char                    sin_zero[8];      //预留,现在还没用到

};

 

(2)       IPv6的套接字地址结构,以sockaddr_in6命名

struct in6_addr {

         uint8_t              s6_addr[16];             //128位网络字节序的IP地址

};

 

struct sockaddr_in6 {

         uint8_t              sin6_len;           // 结构体的长度

         sa_family_t      sin6_family;     //AF_INET6,任何无符号整型

         in_port_t          sin6_port;         //16位网络字节序的端口号,至少16位的无符号整型

         uint32_t            sin6_flowinfo   //仍然是个研究课题,不用。。

struct in6_addr        sin6_addr;                 //128位网络字节序的IP地址

uint32_t            sin6_scope_id;                   //标识地址范围

};

 

通用套接字地址结构,像bind()函数,它需要一个通用型套接字地址结构的指针作参数,所以要将ipv4,ipv6套接字地址结构的指针强制转换为通用套接字地址结构指针。

struct sockaddr {

         uint8_t              sa_len;              //结构体的长度

         sa_family_t      sa_family;         //协议族

         char                    sa_addr[14];    //特定的协议地址

};

         但是,上面的结构体不够存ipv6的128位的ip地址,于是又定义了一个新的通用套接字地址struct sockaddr_storage,它足以容纳系统所支持的任何套接字地址结构,至于如何实现,对用户透明,有兴趣的童鞋可以研究一下。

struct sockaddr_storage {

         uint8_t              ss_len;

         sa_family_t      ss_family;

         ……                                                 //对用户透明

};

 

         对于用户来说,一般只要初始化一个IPv4或IPv6的套接字地址结构,然后用bind()把它邦定到相应的套接字描述符就好了。

 

二、编写TCP套接字程序的一般步骤

套接字应用程序都是客户/服务器(C/S)模式,一个作为服务端,另一个作为客户端,连接成功后,再互相接收/发送数据(全双工)进行通信。

首先,编写一个服务端,一般步骤如下:

(1)       使用socket()创建TCP套接字。

(2)       利用bind()给套接字邦定IP地址,分配端口号。

(3)       使用listen()告诉系统监听在该端口号的连接请求。

(4)       循环执行以下操作:

调用accept()接收每个连接请求,成功后返回新的连接套接字。

使用send()和recv()通过新的连接套接字与客户通信。

通信完成,调用close()关闭该客户连接。

 

         TCP的通信原理都封装在这些函数里了,这些函数只是使用它的接口,如:listen()表示监听某端口上的连接请求,accept()返回一个非负整数表示连接建立成功,close()实现关闭连接等等。下面简单介绍一下这些函数。

☆socket(),上一节已经介绍过了。

☆bind()

#include<sys/socket.h>

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

参数说明:

         sockefd    要邦定协议地址的套接字描述符——由socket()生成。

         myaddr    指向特定的协议地址的指针。

         addrlen    该协议地址的长度

返回值:成功为0,出错则为-1

         对于服务器而言,一般是邦定服务器本机的IP地址和一个众所周知的端口号,在这个套接字上就只监听来自该IP地址和端口的连接请求。众所周知,指的是客户端都知到这个端口提供什么样的服务。对于TCP,调用bind()函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。若不指定端口号,系统内核会为它选择一个临时的端口号,这种情况对客户端来说是正常的,但是对于服务端却极为罕见,因为服务器的端口号都应该是客户端众所周知的,应该具体指定。若不指IP地址,对客户端而言,内核会选择所用外出网络接口作为源IP地址;对服务端而言,内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址。

☆listen()——对套接字sockfd一直监听连接请求

#include<sys/socket.h>

int listen(int sockfd, int backlog);

参数说明:

    sockfd  前面socket()生成的套接字描述符

    backlog 最大的可连接数,是未完成的连接数和已建立连接的连接数两者总和的最大值。

返回值:成功为0,出错为-1

 

☆accept()——接受sockfd上的连接请求

#include<sys/socket.h>

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

参数说明:

    sockfd  前面socket()生成的套接字描述符

    cliaddr 指向已连接的客户进程的协议地址的指针

    addrlen 指向已连接的客户进程的协议地址长度的指针

返回值:若成功,为已连接的新的用来与客户端进行通信的套接字描述符;若出错,为-1.

    如果对客户进程的协议地址不感兴趣,可以将后面两个参数都设为NULL。

☆recv()/send()—接收/发送数据

#include<sys/socket.h>

ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

参数说明:

    前面三个参数和read()/write()的参数一样。这里再说下flags参数,一般设为0,或者需要可以设为以下几个值:

flags               说明            recv()          send()

MSG_DONTROUTE   绕过路由表查找      无              有

MSG_DONTWAIT     仅本操作非阻塞              有                                 有

MSG_OOB                 带外数据                            有                                 有

MSG_PEEK                 窥看处来消息                   有                                 无

MSG_WAITALL                   等待所有数据                   有                                 无

返回值:若成功为读入或者写出的字节数,若出错则为-1

                   若recv()返回值为0,则说明对端已经关闭了TCP连接。

 

☆close()—关闭套接字,并且终止TCP连接.

#include<unistd.h>

int close(int sockfd);

参数:sockfd, 套接字描述符

返回值:成功为0,失败为-1.

 

然后,再编写客户端,一般步骤如下:

(1)       使用socket()创建TCP套接字。

(2)       使用connect()建立到达服务器的连接。

(3)       使用send()、recv()通信。

(4)       使用close()关闭连接。

基本上这几个函数在上面都介绍过了,这里再说说connect()函数。

#include<sys/socket.h>

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

参数说明:

         sockfd       待连接的套接字描述符

         servaddr  向服务端请求连接的服务器地址结构的指针

         addrlen    服务器地址结构的长度

返回值:成功为0,失败为-1;

若connect()成功返回,说明建立连接的三次握手完成,连接已经建立成功,可以进行通信了。

注意:若connect()失败,则该sockfd套接字不能再用,必须关闭,不能对这样的套接字再调用connect()进行连接,必须调用close(sockfd)关闭它,再重新调用socket()生成一个新的套接字进行重连。

 

三、简单实例:服务端回射客户端程序

现在结合一个学习小例子,就是服务端将客户端发送的内容再回射给客户端,对知识点的体会深入浅出。

首先,编写一个服务端,简易代码如下:

/* server.c */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 1024
#define BACKLOG 50

int main(int argc, char** argv)
{   int    listenfd, connfd;    
	struct sockaddr_in  servaddr, cliaddr;    
	char    buff[MAXLINE];    
	int     n = 0;    
	socklen_t clilen;
	
	if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){    
		printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);    
		exit(0);    
	}    
	memset(&servaddr, 0, sizeof(servaddr));    
	servaddr.sin_family = AF_INET;    				//IPv4
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 	//填入本机的IP地址   
	servaddr.sin_port = htons(6666);    			//将端口号转换为网络字节序
	if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){    
		printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);    
		exit(0);    
	}
	if( listen(listenfd, BACKLOG) == -1){    
		printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);    
		exit(0);    
	}
	clilen = sizeof(cliaddr);
	printf("======waiting for client's connect requestion======\n");    
	while(1){   		 
		if( (connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) == -1){        
			printf("accept socket error: %s(errno: %d)",strerror(errno),errno);        
			continue;    
		}    
		fprintf(stdout, "Connected, client addr: %s\n", inet_ntoa(cliaddr.sin_addr));
		if((n = recv(connfd, buff, MAXLINE, 0)) < 0) {
			printf("Failed to receive bytes from client\n");
			exit(-1);
		}    
		buff[n] = '\0';    
		fputs("recv msg from client: ", stdout);
		fputs(buff, stdout);
		while (n > 0) {
			if (send(connfd, buff, n, 0) != n) {
				printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);    
				exit(-1);    
			} 
			if((n = recv(connfd, buff, MAXLINE, 0)) < 0) {
				printf("Failed to receive bytes from client\n");
				exit(-1);
			}    
			buff[n] = '\0';    
			fputs(buff, stdout);
		}
		close(connfd);    
	}
	close(listenfd);
}

 

再编写一个客户端,简易代码如下:

/* client.c */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define MAXLINE 1024

int main(int argc, char** argv)
{   int    sockfd, n, received; 
	int len, bytes;   
	char    recvline[MAXLINE], sendline[MAXLINE];    
	struct sockaddr_in    servaddr;   
	
	if( argc != 2){    
		printf("usage: ./client <ipaddress>\n");    
		exit(0);    
	}    
	memset(recvline, 0, MAXLINE);
	memset(sendline, 0, MAXLINE);
	if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){    
		printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);    
		exit(0);    
	}    
	memset(&servaddr, 0, sizeof(servaddr));    
	servaddr.sin_family = AF_INET;    
	servaddr.sin_port = htons(6666);   //把16位值从主机字节序转换成网络字节序 
	if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){   // [将“点分十进制”ip-> 网络字节序“整数”ip] 
		printf("inet_pton error for %s\n",argv[1]);    
		exit(0);    
	}    
	if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){    
		printf("connect error: %s(errno: %d)\n",strerror(errno),errno);    
		exit(0);    
	}    
	printf("send msg to server: \n");  
	while(1) {  
		fgets(sendline, MAXLINE, stdin);   
		len = strlen(sendline); 
		if( send(sockfd, sendline, len, 0) != len) {    
			printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);    
			exit(0);    
		}
		//接收从服务端的返回数据 	
		fputs("echo from server:\n", stdout);
		received = 0;
		while(received < len) {
			bytes = 0;
			if ((bytes = recv(sockfd, recvline, MAXLINE-1, 0)) == -1 ){
				perror("recv");
				exit(0);
			}
			else if(bytes == 0) {
				printf("recv fail:the server has closed the connection prematually!\n");
				exit(0);
			}
			received += bytes;
			recvline[bytes] = '\0';
			fputs(recvline, stdout);
		}
		fputs("\n", stdout); 
	}	
	close(sockfd);    
	exit(0);
}

编译好后,为了方便,就在一台机子上测试,可以打开两个命令行控制终端,一个运行服务端,一个运行客户端。

先启动服务端:

[lgh@localhost socket]$ ./server1

======waiting for client's connect requestion======

再启动客户端,输入“hello tcp!”,然后服务端又它回射过来。

[lgh@localhost socket]$ ./client1 192.168.1.107

send msg to server:

hello tcp!

echo from server:

hello tcp!

         此时,服务端的结果:

         Connected, client addr: 192.168.1.107

recv msg from client: hello tcp!

 

------------------------------------------------------------------------------------- <end>

 

抱歉!评论已关闭.