Socket编程(1)
编写环境为Windows 不过在Linux略微修改就行了,这个真的不是在做课程实验
实现目标
??实现服务端Server与客户端Client,有客户端向服务端发起通信,服务端能够进行响应。
实现思路
??使用TCP协议进行实现,所以在整个通信流程中,很显然需要让客户端知道怎么与服务端发起通信(也就是怎么找到服务端)。 ??服务端需要确定IP地址和端口号,例如本机IP可以用127.0.0.1这类127开头的环回地址(也就是localhost)或者使用ipconfig查询IP地址,以及在服务端运行时的端口号。 ??客户端通过socket套接字向服务端的IP和端口建立连接,进行通信。
代码实现
服务端
创建服务端的套接字并返回createsockfd
??默认端口号12138(当然只是我自己写乱填的,只要不是系统保留的端口也别大于65535就可以)。 ??WSADATA根据百度百科的描述是(一种用于存储被WSAStartup函数调用后返回的Windows Sockets数据的数据结构)。 ??套接字SOCKET变量名为fd。使用SOCKET socket(int af, int type, int protocol)函数建立,第一个参数为地址族,IPV4即AP_INET,第二个参数为协议类型,SOCK_STREAM为流式套接字,第三个参数填写为IPPROTO_TCP使用TCP协议(填0时系统会根据套接字的类型决定应使用的传输层协议,当前仅TCP可选),判断返回值是否合法。 ??结构体sockaddr_in和sockaddr定义如下,分别用于存储地址族类型、IP地址、端口号最后一个不使用(用于与结构体sockaddr的大小保持一致),其中端口号和地址需要使用网络字节序(大端法),要注意主机中的字节序是否一致,使用hton()函数进行转换。
struct sockaddr_in {
ADDRESS_FAMILY sin_family;
USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
}
struct sockaddr {
ADDRESS_FAMILY sa_family;
CHAR sa_data[14];
}
??sin_famaily设置为AF_INET,端口号使用htons函数将主机字节序转换为网络字节寻进行存放,地址监听任意网卡也就是全0的地址,可以查看INADDR_ANY的定义。
#define INADDR_ANY (ULONG)0x00000000
??使用bind函数将地址与套接字fd进行绑定,其中第二个参数要使用类型转换否则编译报错。使用listen函数设置fd的监听队列,其中第二个参数大小为TCP三次握手后完成的任务队列大小即accept queue的大小,还有一个队列syn queue为三次握手过程中的任务队列大小,参数在系统中设置。若正确完成则返回fd即可。
SOCKET createsockfd(int port=12138)
{
WSADATA wd;
WSAStartup(MAKEWORD(2, 2), &wd);
SOCKET fd;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
{
printerror((char*)"建立SOCKET时错误码:");
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons((unsigned short)port);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(fd, (struct sockaddr*)&serveraddr, sizeof(sockaddr_in))!=0)
{
closesocket(fd);
printerror((char*)"bind函数时错误码");
}
if (listen(fd, 23) != 0)
{
closesocket(fd);
printerror((char*)"listen函数时错误码");
}
return fd;
}
服务端处理连接 handle
??传入参数为先前建立的套接字fd,buffer为存放数据的空间大小(这个服务端代码目前只能同时处理一个客户端的请求,其余客户端会在三次握手后的accept队列中排队),sockconn为用于与客户端连接的套接字,clientaddr用于存放客户端的相关信息如客户端地址等。 ??第一个while(1)为服务端能够持续处理客户端连接。 ??调用accept函数从fd监听的accept队列中获取待处理任务的套接字,accept函数当前为阻塞模式,当accept队列为空时会阻塞,后两个参数不需要知道客户端信息时可以填NULL。输出客户端地址,因为是网络字节序,将其转为主机的字节序后,将字符串进行输出。 ??第二个while(1)用于与客户端持续通信,recv函数从sockconn套接字中接收数据,第二个参数为缓冲区存放recv接收到的地址,第三个参数为buffer的长度(不是接收信息的长度),第四个参数一般为0,返回值为实际接收到的字节数,接收到内容后判断是否为close,若是则结束并关闭连接,不是则向客户端发送“成功接收”,其中第三个参数与recv中不同,是要发送的数据长度。
void handle(SOCKET fd)
{
char buffer[1 << 10];
SOCKET sockconn;
struct sockaddr_in clientaddr;
int clientaddrsz=sizeof(sockaddr);
while (1)
{
sockconn = accept(fd, (sockaddr*)&clientaddr, &clientaddrsz);
printf("客户端地址(%s)\n", inet_ntoa(clientaddr.sin_addr));
while (1)
{
memset(buffer, 0, sizeof(buffer));
int state;
if ((state = recv(sockconn, buffer, sizeof(buffer), 0)) < 0)
{
printf("接收错误,结束连接");
break;
}
if (strcmp("close", buffer) == 0)
{
printf("与客户端连接结束");
break;
}
printf("从客户端接收%s\n", buffer);
strcpy(buffer, "成功接收");
if ((state = send(sockconn, buffer, strlen(buffer), 0)) < 0)
{
printf("发送失败,结束连接");
perror("send");
break;
}
printf("向客户端发送%s\n", buffer);
}
closesocket(sockconn);
}
}
服务端主函数
??如果运行参数给定接口则使用给定的接口,否则默认12138,使用creatsockfd函数建立套接字,调用handle函数开始处理连接,结束时关闭套接字。
int main(int argc,char *argv[])
{
if (argc != 2)
{
fd = createsockfd();
printf("port number=12138\n");
}
else
{
fd = createsockfd(atoi(argv[1]));
}
handle(fd);
closesocket(fd);
return 0;
}
客户端
发起连接createconn
??使用参数给定的ip地址和port端口,向该地址的服务器发起连接请求,调用gethostbyname函数,该函数原型struct hostent *gethostbyname(const char *hostname),用于获取ip地址(函数的参数也可以是域名)对应主机,该函数的返回值为存放主机信息的hostent结构体的内存地址。 ??hostent结构体中保存了对应主机的域名,别名,IP地址的地址族,IP长度,及该服务器的可能的多个IP地址,h_addr定义为h_addr_list[0],也就是第一个IP地址。
struct hostent {
char FAR * h_name;
char FAR * FAR * h_aliases;
short h_addrtype;
short h_length;
char FAR * FAR * h_addr_list;
#define h_addr h_addr_list[0]
};
??将host中的IP信息复制到serveraddr中,以及端口号(注意主机字节序和网络字节序的转换)和地址族,使用connect函数向服务端建立连接,第一个参数为套接字,第二个参数为服务端信息,第三个参数为sockaddr长度,若成功建立连接后推出返回fd。
SOCKET createconn(char ip[], int port)
{
WSADATA wd;
WSAStartup(MAKEWORD(2, 2), &wd);
SOCKET fd;
if ((fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
printerror((char*)"建立SOCKET时错误码:");
}
struct hostent* host;
if ((host = gethostbyname(ip)) == 0)
{
printf("gethostbyname错误\n");
closesocket(fd);
exit(0);
}
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
memcpy(&serveraddr.sin_addr, host->h_addr, host->h_length);
serveraddr.sin_port = htons((unsigned short)port);
serveraddr.sin_family = AF_INET;
if (connect(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr))==SOCKET_ERROR)
{
perror("connect");
closesocket(fd);
exit(0);
}
return fd;
}
通信过程
??与服务端的通信部分相似,仅发送的数据为用户输入,结束后关闭sockconn的连接。
void handle(SOCKET sockconn)
{
char buffer[1 << 10];
printf("最多输入长度1024,输入close结束连接\n");
while (1)
{
int state;
memset(buffer, 0, sizeof(buffer));
scanf("%s", buffer);
if ((state = send(sockconn, buffer, strlen(buffer), 0)) <= 0)
{
printf("发送失败,结束连接");
perror("send");
break;
}
if (strcmp("close", buffer) == 0)
{
printf("与服务端连接结束");
break;
}
printf("向服务端发送%s\n", buffer);
if ((state = recv(sockconn, buffer, sizeof(buffer), 0)) < 0)
{
printf("接收错误,结束连接");
break;
}
printf("从服务端接收%s\n", buffer);
}
closesocket(sockconn);
}
客户端主函数
??服务端的端口号可以默认,但是客户端发起连接的IP和端口号不能为空,不然不知道找哪个服务端,建立连接后进行通信。
int main(int argc,char* argv[])
{
if (argc != 3)
{
printf("输入地址及端口号");
return 0;
}
SOCKET sockconn=createconn(argv[1], atoi(argv[2]));
handle(sockconn);
return 0;
}
总结
??这两份代码是简单的套接字通信,还有很多可以改进的地方,在日后逐一完善。
|