Sockaddr数据结构
strcut sockaddr以前是一个结构体类型,但是现在不用来做结构体了,而是作为(void*)指针,传递一个地址给函数,即:当要使用sockaddr_in 或者sockaddr_in6作为bind、connect、accept函数的参数传递时,要讲它们强制转换为strcut sockaddr类型,即(strcut sockaddr*)加函数地址。
接下来看看sockaddr_in结构体里有什么:
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family // 表示你所用选用的地址协议族
__be16 sin_port; /* Port number // 端口号
struct in_addr sin_addr; /* Internet address // IP 地址,这是一个结构体,里面只有一个成员,如下
/还有其他的成员,但是没有上面三个重要/
};
struct in_addr { /* Internet address. */
__be32 s_addr;
};
举个例子: 建立一个sockaddr_in的结构体并初始化成员:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));//将结构体清零
servaddr.sin_family = AF_INET; //或者ipv6就赋值为AF_INET6
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //初始化IP地址,需要用大小端的转换函数,或inet_pton。网络地址设为 INADDR_ANY,这个宏表示本地的任意 IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址。
servaddr.sin_port = htons(6666); //初始化端口:需要用大小端的转换函数:由于这个数据包接下来应该被投放到网络当中去,所以用的是htons函数,而不是ntohs
bind( , (strcut sockaddr*)& servaddr);//在执行其他函数时,要用到该结构体作为参数,需要进行强制类型转换成strcut sockaddr
如果是要返回IP或端口号给用户自己看,那就是相反了,用的应该是ntohs或者inet_ntop函数。
socket函数
(1)
int socket(int domain, int type, int protocol);
创建一个新的套接字,并返回出可以用来访问它的文件描述符
(2)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
给套接字绑定IP和端口号(即存放IP和端口号的结构体)使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。
如果不调用bind函数的话,系统也会自动给该个终端分配一个IP地址和端口号,因此客户端可以不调用bind进行绑定,但是服务端必须调用,如果不调用的话客户端就找不到服务端了。
(3)
int listen(int sockfd, int backlog);
指定该套接字能同时连接的上限数。比如传入backlog为128,则表示同时允许128个客户与我连接,当第129个进来时,需要排队等待,等待前128个的有客户有连接完成后,才能进来连接。
(4)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
三方握手完成后,服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。成功返回一个新的 socket 文件描述符(注意是新的socketfd),用于和该个连接上的客户端通信,失败返回-1,设置 errno。
参数struct sockaddr *addr代表的是客户端的IP和端口,也就是说传入前不需要对它进行初始化,它是一个传出参数,返回链接客户端地址信息,含 IP 地址和端口号。
参数socklen_t *addrlen:传入传出参数(在函数中读,然后发生改变,然后改变后的量传出),传入的是调用者提供的缓冲区 addr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 addr 参数传 NULL,表示不关心客户端的地址。
(5)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这是客户端调用的connect()连接服务器,传入服务端的地址结构体addr。
传入的参数int sockfd是客户端自己的,因此在客户端的程序中也需要调用socket函数建一个套接字。传入的参数const struct sockaddr *addr是服务器的。
connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,
而 connect 的参数是对方的地址。
对于服务端和客户端的连接过程,作一个小结:
对于服务端:
(1) 调用socket函数创建套接字
(2) 调用bind函数绑定IP和端口号,即初始化struct sockaddr_in的后续操作
(3) 调用listen函数指定同时和该客户端连接的最大连接数
(4) 调用accept函数阻塞等待客户端发起连接
(5) 读取客户端发送来的数据
(6) 处理数据
(7) 回发数据给客户端
(8) close
对于客户端:
(1) 调用socket函数创建套接字
(2) 调用bind函数绑定IP和端口号,也可以不显示调用,依赖于“隐式绑定”
(3) 调用connect函数,传入服务端的struct sockaddr_in,对其发起连接
(4) 发送数据给服务端
(5) 读取服务端回发的数据
(6) close
代码例子:实现服务端等待连接客户端,连接成功后客户端可以给服务端发送字母,服务端接收成功后转成大写再发回客户端。
/*创建一个服务器:server.c*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<ctype.h>//里面放的是大小字母转换函数
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8001
int main()
{
struct sockaddr_in serveraddr,clientaddr;//定义服务器和客户端的sockaddr数据结构
int sockfd;//本地服务器的套接字(文件描述符)
int confd;//连接上客户端的套接字(文件描述符)
socklen_t client_addrlen;//客户端sockaddr数据结构的大小
char buf[BUFSIZ] = "0";//BUFSIZ这个宏是表示系统默认的缓冲区大小
int iError=0;
/*1.socket*/
sockfd = socket(AF_INET,SOCK_STREAM,0); //创建一个socket,指定IPv4协议族_TCP协议
if(sockfd==-1)
{
printf("sockfd is error!\n");
exit(-1);
}
/*2.先初始化服务器的地址结构,再进行bind*/
bzero(&serveraddr,sizeof(serveraddr)); //将整个结构体清零
serveraddr.sin_family = AF_INET;//选择协议族为IPv4
serveraddr.sin_port = htons(SERVER_PORT);//绑定端口号
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有ip地址
iError=bind(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));//绑定
if(iError==-1)
{
printf("bind失败\n");
exit(-1);
}
/*3.listen设定链接上限,注意此处不阻塞*/
listen(sockfd,64);//同一时刻允许向服务器发起链接请求的数量
/*4.accept阻塞监听客户端链接请求*/
printf("接下来等待客户端连接本服务器,socketfd=%d,severIP=%s,severPort=%d\n",sockfd,inet_ntop(AF_INET,&serveraddr.sin_addr.s_addr,buf,sizeof(buf)),ntohs(serveraddr.sin_port));
// bzero(&clientaddr,sizeof(clientaddr)); //将客户端整个结构体清零
client_addrlen = sizeof(clientaddr);
confd = accept(sockfd,(struct sockaddr*)&clientaddr,&client_addrlen);//监听客户端链接, 会阻塞
if(confd==-1) printf("连接失败\n");
else printf("连接成功,客户端IP:%s,端口号:%d\n",inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,buf,sizeof(buf)),ntohs(clientaddr.sin_port));
/*5.处理客户端请求,文件描述符confd*/
while (1)
{
int i = 0;
/*读取客户端发送数据*/
int len = read(confd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);//输出到终端屏幕中
/*处理客户端数据*/
for (i=0;i<len;i++) buf[i]=toupper(buf[i]);//把从客户端发送来的数据小写转大写,并放在缓冲区中
/*处理完数据回写给客户端*/
write(confd,buf,len);
}
/*6.关闭链接*/
close(sockfd);
close(confd);
return 0;
}
/*写一个客户端连接服务端并发送字符串过去:client.c*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main(int argc, char** argv)
{
int sockfd;
struct sockaddr_in serveraddr;
char buf[BUFSIZ];
sockfd = socket(AF_INET,SOCK_STREAM,0);
/*初始化一个地址结构:*/
bzero(&serveraddr, sizeof(serveraddr));//清零
serveraddr.sin_family = AF_INET; //IPv4协议族
inet_pton(AF_INET,argv[1],&serveraddr.sin_addr.s_addr);//字符串类型转换为网络字节序
serveraddr.sin_port = htons(8001); //指定端口 本地转网络字节序
/*根据地址结构链接指定服务器进程*/
printf("接下来连接服务器,sockfd=%d,serverIP=%s,serverPort=%d",sockfd,inet_ntop(AF_INET,&serveraddr.sin_addr.s_addr,buf,sizeof(buf)),ntohs(serveraddr.sin_port));
connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
while (1)
{
int len;
/*从标准输入获取数据*/
fgets(buf,sizeof(buf),stdin);
/*将数据写给服务器*/
write(sockfd,buf,strlen(buf));//写给服务器
/*从服务器读回转换后数据*/
len = read(sockfd,buf,sizeof(buf));
/*写至标准输出(终端屏幕)*/
write(STDOUT_FILENO, buf, len);
}
/*关闭链接*/
close(sockfd);
return 0;
}
执行结果:
备注:有时候在虚拟机中执行第二次的server的时候会出现bind失败的情况,那是因为端口被占用了,此时把代码中的传入服务器结构体的端口号更改一下,或者重启一下虚拟机就可以继续连接了。
或者在终端执行命令 “netstat -anp |grep 端口号”来查看当前被占用的端口号的进程
然后用“kill -s 9 pid号” 释放进程