【C语言】Socket网络编程与TCP/IP

一、网络中的进程(应用程序)通信
       同一主机上,进程间可通过PID(进程号)唯一标识,通过pipe、FIFO、信号量、共享内存、Socket等方式进行通信;但是网间主机的通信不可通过进程号唯一标识,网间进程通信要解决的是不同主机进程间的标识问题。
       TCP/IP协议族已经帮我们解决了这个问题。网络层的“IP地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的进程。TCP/IP协议的应用程序通常采用Socket编程接口来实现网络进程之间的通信。
二、Socket(套接字)
       Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
       Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作,Socket就是该模式的一个实现。socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。在网络编程中,几乎离不开socket接口。接口的实现由内核来完成。
三、TCP/IP与Socket套接字
      
Socket在TCP/IP中的位置
四、相关数据结构和函数
1.
(1)sockaddr_in结构体(IPV4,常用)
struct sockaddr_in
{
   short int sin_family; //协议类型
   unsigned short int sin_port; //端口号
   struct in_addr sin_addr; //IP地址
   unsigned char sin_zero[8]; // 填充0 以保持与struct sockaddr同样大小
};
struct in_addr
{
       uint32_t s_addr; //网络字节序形式的IP地址
};
(2)sockaddr_in6 结构体
struct sockaddr_in6
{
       sa_family_t sin6_family; //AF_INET6
       in_port_t sin6_port; //port number
       uint32_t sin6_flowinfo; // IPv6 flow information
       struct in6_addr sin6_addr; //IPv6 address
       uint32_t sin6_scope_id; // Scope ID
};
struct in6_addr
{
       unsigned char s6_addr[16]; /* IPv6 address */
};
(3)sockaddr结构体
struct sockaddr
{
       unsigned short sa_family; //通信协议类型族AF_xx
       char sa_data[14]; //14字节协议地址,包含该socket的IP地址和端口号
};
两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。socket数据结构中包含这五种信息。
2.socket函数
原型:int socket(int family,int type,int protocol)
描述:调用成功,返回socket文件描述符;调用失败,返回-1,并设置errno
family:
指定使用的协议簇:AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(UNIX协议)、 AF_ROUTE(路由套接字)、 AF_KEY(秘钥套接字)等
type:
指定使用的套接字的类型:SOCK_STREAM(字节流套接字、TCP)、SOCK_DGRAM(数据报套接字、UDP)、SOCK_RAW(原始套接字)等
protocol:
常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。如果套接字类型不是原始套接字,那么这个参数一般为0。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
3.bind函数
原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述:将socket套接字和指定的端口相连。成功返回0,否则,返回-1,并置errno
sockfd:
socket函数返回的套接字描述符。可以唯一标识一个socket
addr:
是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针
myaddrlen:
sockaddr结构体长度

4.listen函数
原型:int listen(int sockfd, int backlog);
描述:等待指定的端口的出现客户端连接。调用成功返回0;否则,返回-1,并置errno
sockfd:
要监听的套接字描述符sockfd。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求
backlog:
设置可连接客户端的最大连接个数,当有多个客户端向服务器请求时,受到此值的影响。默认值为20
5.accept函数
原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
描述:用于接受客户端的服务请求,成功返回新的套接字描述符,失败返回-1,并置errno
sockfd:
被监听的socket描述符
addr:
结果参数,它用来接受一个返回值,返回指定客户端的地址。如果对客户的地址不感兴趣,可以把这个值设置为NULL。
addrlen:
也是结果的参数,用来接受上述addr结构的大小的,它指明addr结构所占有的字节个数。同样也可以被设置为NULL。
6.connect函数(客户端连接服务器使用)
原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述:客户端向服务器发送服务请求。成功返回0,否则返回-1,并置errno
sockfd:
socket函数返回套接字描述符
serv_addr:
服务器主机地址结构体指针
addrlen:
结构体指针的长度
7.网络I/O读写函数
(1) #include <unistd.h>
读:ssize_t read(int fd, void *buf, size_t count);
写:ssize_t write(int fd, const void *buf, size_t count);
(2)#include <sys/types.h>、 #include <sys/socket.h>
读: ssize_t recv(int sockfd, void *buf, size_t len, int flags);
写:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
读:ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
写:ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
读:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags);
写:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags);

read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0 表示已经读到文件的结束了,小于0表示出现了错误,并置errno。如果错误为EINTR说明读是由中断引起的,如果错误是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数.失败时返回-1.,并设置errno变量。
其他的IO函数也可以使用,可以在linux系统下通过man命令查看使用方法。
8.close函数
原型:int close(int fd);
描述:在通信完后应及时释放连接、资源,调用close关闭socket连接。成功返回0,否则返回-1。

五、socket通信流程
(1)服务器
struct sockaddr_in(描述服务器地址信息)->socket(新建套接字)->bind(绑定套接字描述符和地址信息)->listen(进入监听状态,监听指定端口)->accept(调用后进入阻塞状态,等待用户连接后返回一个连接sockfd)->send/recv/read/write(向文件缓冲区读写信息)->close(四次挥手,释放连接)
(2)客户端
struct sockaddr_in(描述连接的服务器地址信息)->socket(新建套接字)->connect(三次握手,连接服务器)->read/write(向文件缓冲区读写信息)->close(四次挥手,释放连接)

通常作为服务器的一方要绑定一个客户端知道的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,系统会自动分配一个端口号和自身的ip地址组合作为客户端地址。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
在这里插入图片描述

Socket编程例子(linux环境):本机内的两个进程进行通信,本地IP地址和8000端口作为服务器进程的地址。如果服务器收到客户端连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。

服务器:

#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 SERVER_PORT 8000  
#define MAXLINE 4096  
int main(int argc, char** argv)  
{
    
      
    int    socket_fd, connect_fd;  
    struct sockaddr_in     servaddr;  
    char    buff[4096];  
    int     n;  
    //初始化Socket  
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )           //TCP
    {
    
      
	    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_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
    servaddr.sin_port = htons(SERVER_PORT);//设置的端口为8000  
  
    //将本地地址绑定到所创建的套接字上  
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
    {
    
      
	    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
	    exit(0);  
    }  
    //开始监听是否有客户端连接  
    if( listen(socket_fd, 10) == -1)
    {
    
      
	    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
	    exit(0);  
    }  
    printf("======waiting for client's request======\n");  
    while(1)
    {
    
      
	//阻塞直到有客户端连接,不然多浪费CPU资源。  
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1)
        {
    
      
	        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);  
	        continue;  
    	}  
	//接受客户端传过来的数据  
	    n = recv(connect_fd, buff, MAXLINE, 0);  
	          
	//向客户端发送回应数据  
	    if(!fork())            //子进程执行
	    {
    
     
	        if(send(connect_fd, "I have received message!\n", 26,0) == -1)  
	        perror("send error");  
	        close(connect_fd);  
	        exit(0);  
	    }  
    buff[n] = '\0';  
    printf("recv msg from client: %s\n", buff);  
    close(connect_fd);  
    }  
    close(socket_fd);  
}  

客户端

#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 4096  
  
  
int main(int argc, char** argv)  
{
    
      
    int    sockfd, n,rec_len;  
    char    recvline[4096], sendline[4096];  
    char    buf[MAXLINE];  
    struct sockaddr_in    servaddr;  
  
  
    if( argc < 2)
	{
    
      
	    printf("usage: ./client <ipaddress>\n");  
    	exit(0);  
    }  
  
  
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)					//TCP
    {
    
      
	    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(8000);  
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    {
    
      
	    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("you have connected server!\nserver IP Address:%s \n",argv[1]);  
  
    printf("send msg to server: \n");  
    fgets(sendline, 4096, stdin);  
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)  
    {
    
      
	    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
	    exit(0);  
    }  
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) 
    {
    
      
       perror("recv error");  
       exit(1);  
    }  
    buf[rec_len]  = '\0';  
    printf("server : %s ",buf);  
    close(sockfd);  
    exit(0);  
}  

先运行服务器监听端口
在这里插入图片描述
再运行客户端连接服务器,相互通信。
在这里插入图片描述

六、其他相关函数
1.网络字节顺序及其转换函数
(1) 网络字节顺序
       每一台机器内部对变量的字节存储顺序不同,而网络传输的数据是一定要统一顺序的。所以对内部字节表示顺序与网络字节顺序不同的机器,一定要对数据进行转换,从程序的可移植性要求来讲,就算本机的内部字节表示顺序与网络字节顺序相同也应该在传输数据以前先调用数据转换函数,以便程序移植到其它机器上后能正确执行。真正转换还是不转换是由系统函数自己来决定的。
(2) 有关的转换函数
#include <arpa/inet.h>
unsigned short int htons(unsigned short int hostshort):
主机字节顺序转换成网络字节顺序,对无符号短型进行操作4bytes
unsigned long int htonl(unsigned long int hostlong):
主机字节顺序转换成网络字节顺序,对无符号长型进行操作8bytes
unsigned short int ntohs(unsigned short int netshort):
网络字节顺序转换成主机字节顺序,对无符号短型进行操作4bytes
unsigned long int ntohl(unsigned long int netlong):
网络字节顺序转换成主机字节顺序,对无符号长型进行操作8bytes

2.IP地址转换
       有三个函数将数字点形式表示的字符串IP地址与32位网络字节顺序的二进制形式的IP地址进行转换
(1) unsigned long int inet_addr(const char * cp):该函数把一个用数字和点表示的IP地址的字符串转换成一个无符号长整型,如:struct sockaddr_in ina
ina.sin_addr.s_addr=inet_addr(“202.206.17.101”)
该函数成功时:返回转换结果;失败时返回常量INADDR_NONE,该常量=-1,二进制的无符号整数-1相当于255.255.255.255,这是一个广播地址,所以在程序中调用iner_addr()时,一定要人为地对调用失败进行处理。由于该函数不能处理广播地址,所以在程序中应该使用函数inet_aton()。
(2)int inet_aton(const char * cp,struct in_addr * inp):此函数将字符串形式的IP地址转换成二进制形式的IP地址;成功时返回1,否则返回0,转换后的IP地址存储在参数inp中。
(3) char * inet_ntoa(struct in-addr in):将32位二进制形式的IP地址转换为数字点形式的IP地址,结果在函数返回值中返回,返回的是一个指向字符串的指针。

3.字节处理函数
       Socket地址是多字节数据,不是以空字符结尾的,这和C语言中的字符串是不同的。Linux提供了两组函数来处理多字节数据,一组以b(byte)开头,是和BSD系统兼容的函数,另一组以mem(内存)开头,是ANSI C提供的函数。
以b开头的函数有:
#include <strings.h>
(1) void bzero(void * s,int n):将参数s指定的内存的前n个字节设置为0,通常它用来将套接字地址清0。
(2) void bcopy(const void * src,void * dest,int n):从参数src指定的内存区域拷贝指定数目的字节内容到参数dest指定的内存区域。
(3) int bcmp(const void * s1,const void * s2,int n):比较参数s1指定的内存区域和参数s2指定的内存区域的前n个字节内容,如果相同则返回0,否则返回非0。

以mem开头的函数有:
#include <string.h>
(1) void * memset(void * s,int c,size_t n):将参数s指定的内存区域的前n个字节设置为参数c的内容。
(2) void * memcpy(void * dest,const void * src,size_t n):功能同bcopy(),区别:函数bcopy()能处理参数src和参数dest所指定的区域有重叠的情况,memcpy()则不能。
(4) int memcmp(const void * s1,const void * s2,size_t n):比较参数s1和参数s2指定区域的前n个字节内容,如果相同则返回0,否则返回非0。

猜你喜欢

转载自blog.csdn.net/yechongbinbin/article/details/114313166